From 1a0468b19e8da2a7ed92502f2bca7a4862828d41 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 06:28:40 +0000 Subject: [PATCH 01/13] feat: S08.07 slice 1 MbedTlsStream pool scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-TU split mirroring OpenSSL TlsStream; INTERFACE pack like FreeRtos/FatFs because mbedtls_config.h is header-configured. Initialise/Cleanup are no-ops that wire the Stream vtable to NullStream — real Open/Send/Read/Close lands in slice 2 via mbedtls_ssl_set_bio. Config struct accepts pre-built mbedTLS handles (mbedtls_ctr_drbg_context*, mbedtls_x509_crt*, mbedtls_pk_context*) as forward declarations so the public header has zero mbedTLS includes. Library policy lives on the per-instance config; no process-global mbedTLS calls in Platform/MbedTls/Source/ (coexistence contract). Tests: 10 pool tests parity with SolidSyslogTlsStreamPoolTest (Create, exhaustion, NullStream fallback, Send drop-on-floor, ConfigLock pair, Destroy pair, unknown + stale destroy warnings). Tests/MbedTlsIntegration/ links real libmbedtls from \$MBEDTLS_DIR as a subproject; smoke test confirms the link, in-process TLS server lands in slice 3. Pre-commit (freertos-host container): format clean, tidy clean on MbedTls targets, cppcheck clean, sanitize clean. IWYU skipped (needs cpputest-clang). Story: #272. Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 24 +++ Core/Interface/SolidSyslogTunablesDefaults.h | 23 +++ Core/Source/SolidSyslogErrorMessages.h | 4 + Platform/MbedTls/CMakeLists.txt | 29 +++ .../Interface/SolidSyslogMbedTlsStream.h | 35 ++++ .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 30 +++ .../Source/SolidSyslogMbedTlsStreamPrivate.h | 16 ++ .../Source/SolidSyslogMbedTlsStreamStatic.c | 72 +++++++ Tests/CMakeLists.txt | 14 ++ Tests/MbedTls/CMakeLists.txt | 32 ++++ .../SolidSyslogMbedTlsStreamPoolTest.cpp | 180 ++++++++++++++++++ Tests/MbedTls/main.cpp | 6 + Tests/MbedTlsIntegration/CMakeLists.txt | 76 ++++++++ ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 23 +++ Tests/MbedTlsIntegration/main.cpp | 6 + 15 files changed, 570 insertions(+) create mode 100644 Platform/MbedTls/CMakeLists.txt create mode 100644 Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c create mode 100644 Tests/MbedTls/CMakeLists.txt create mode 100644 Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp create mode 100644 Tests/MbedTls/main.cpp create mode 100644 Tests/MbedTlsIntegration/CMakeLists.txt create mode 100644 Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp create mode 100644 Tests/MbedTlsIntegration/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 59513f2d..8fd53fba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,26 @@ if(SOLIDSYSLOG_OPENSSL) endif() endif() +# mbedTLS-based TLS is the embedded-target reference TLS backend (OpenSSL is too +# large for Cortex-M). Default ON when the MBEDTLS_DIR environment variable is +# set (the cpputest-freertos devcontainer image provides /opt/mbedtls); pass +# -DSOLIDSYSLOG_MBEDTLS=OFF to disable explicitly. Unlike OpenSSL there is no +# find_package — mbedTLS is header-configured (mbedtls_config.h changes ABI), +# so the adapter ships as an INTERFACE pack and consumers compile the sources +# themselves with their own config. See Platform/MbedTls/CMakeLists.txt. +if(DEFINED ENV{MBEDTLS_DIR}) + set(_solidsyslog_mbedtls_default ON) +else() + set(_solidsyslog_mbedtls_default OFF) +endif() +option(SOLIDSYSLOG_MBEDTLS "Include mbedTLS-based TLS support" ${_solidsyslog_mbedtls_default}) +unset(_solidsyslog_mbedtls_default) +if(SOLIDSYSLOG_MBEDTLS AND NOT DEFINED ENV{MBEDTLS_DIR}) + message(FATAL_ERROR + "SOLIDSYSLOG_MBEDTLS=ON but MBEDTLS_DIR is not set; " + "export MBEDTLS_DIR=/path/to/mbedtls or pass -DSOLIDSYSLOG_MBEDTLS=OFF.") +endif() + # Allocation strategy (E11). Currently only "static" ships — internal static # pools allocate instance bookkeeping for stateful classes. A future epic # may add "dynamic" (heap-backed). Selecting "dynamic" now errors hard so @@ -170,6 +190,10 @@ if(SOLIDSYSLOG_OPENSSL) add_subdirectory(Platform/OpenSsl) endif() +if(SOLIDSYSLOG_MBEDTLS) + add_subdirectory(Platform/MbedTls) +endif() + if(HAVE_WINDOWS_INTERLOCKED OR HAVE_WINDOWS_PLATFORM OR SOLIDSYSLOG_WINSOCK) add_subdirectory(Platform/Windows) endif() diff --git a/Core/Interface/SolidSyslogTunablesDefaults.h b/Core/Interface/SolidSyslogTunablesDefaults.h index 0b807764..ebf4e2b7 100644 --- a/Core/Interface/SolidSyslogTunablesDefaults.h +++ b/Core/Interface/SolidSyslogTunablesDefaults.h @@ -647,4 +647,27 @@ #error "SOLIDSYSLOG_TLS_STREAM_POOL_SIZE must be >= 1" #endif +/* + * Number of SolidSyslogMbedTlsStream instances the library's internal static + * pool can simultaneously hold. Each instance carries an mbedtls_ssl_context, + * mbedtls_ssl_config, and the integrator's MbedTlsStreamConfig (transport + * pointer, sleep callback, mbedTLS handle pointers — Rng, CaChain, optional + * ClientCertChain/ClientKey). + * + * Default 1 — TLS senders are scoped per destination and almost all + * integrators wire a single TLS sender per process. Bump via + * SOLIDSYSLOG_USER_TUNABLES_FILE if more than one is genuinely needed + * (e.g. multi-destination egress with separate TLS sessions per peer). + * + * Floor: 1. Sub-floor values rejected at compile time. + */ +#ifndef SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE +/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -- macro form required for preprocessor visibility (floor #if) and C array-size const-expr. */ +#define SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE 1U +#endif + +#if SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE < 1 +#error "SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE must be >= 1" +#endif + #endif /* SOLIDSYSLOG_TUNABLES_DEFAULTS_H */ diff --git a/Core/Source/SolidSyslogErrorMessages.h b/Core/Source/SolidSyslogErrorMessages.h index d9f9ff03..3d9004f5 100644 --- a/Core/Source/SolidSyslogErrorMessages.h +++ b/Core/Source/SolidSyslogErrorMessages.h @@ -141,5 +141,9 @@ "SolidSyslogTlsStream_Create pool exhausted; returning fallback stream" #define SOLIDSYSLOG_ERROR_MSG_TLSSTREAM_UNKNOWN_DESTROY \ "SolidSyslogTlsStream_Destroy called with a handle not issued by this pool" +#define SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_POOL_EXHAUSTED \ + "SolidSyslogMbedTlsStream_Create pool exhausted; returning fallback stream" +#define SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_UNKNOWN_DESTROY \ + "SolidSyslogMbedTlsStream_Destroy called with a handle not issued by this pool" #endif /* SOLIDSYSLOGERRORMESSAGES_H */ diff --git a/Platform/MbedTls/CMakeLists.txt b/Platform/MbedTls/CMakeLists.txt new file mode 100644 index 00000000..a348383b --- /dev/null +++ b/Platform/MbedTls/CMakeLists.txt @@ -0,0 +1,29 @@ +# mbedTLS adapter pack — shipped as sources, not a precompiled library. +# +# mbedTLS is header-configured: the integrator's mbedtls_config.h flows +# through upstream headers and changes the size/layout of mbedtls_ssl_context, +# mbedtls_x509_crt, mbedtls_pk_context, and the visible API surface. +# Compiling our adapter once into libSolidSyslog.a would silently break +# consumers that use a different mbedtls_config.h. +# +# Instead, this is an INTERFACE library — each consumer (Bdd/Targets/FreeRtos, +# Tests/MbedTls/*Test, downstream integrator apps) recompiles the adapter +# sources from Source/ with its own mbedtls_config.h on the include path. +# Mirrors Platform/FreeRtos/ and Platform/FatFs/ — see +# project_header_configured_platforms memory. + +add_library(SolidSyslogMbedTls INTERFACE) + +# No adapter sources at S08.07 slice 1 — first content +# (SolidSyslogMbedTlsStream.h plus the three-TU split) lands in the +# slice-1 green step. The INTERFACE target exists so the directory and +# CMake wiring are in place from the plumbing slice. + +target_include_directories(SolidSyslogMbedTls INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/Interface +) + +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/Interface/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h" +) diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h new file mode 100644 index 00000000..b8ac1216 --- /dev/null +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h @@ -0,0 +1,35 @@ +#ifndef SOLIDSYSLOGMBEDTLSSTREAM_H +#define SOLIDSYSLOGMBEDTLSSTREAM_H + +#include "ExternC.h" +#include "SolidSyslogSleep.h" + +struct SolidSyslogStream; + +/* Forward declarations keep the public header free of any mbedTLS include. + * Integrators include the relevant mbedTLS headers themselves before this + * one to bring the types into scope. See project_mbedtls_di_handles. */ +struct mbedtls_ctr_drbg_context; +struct mbedtls_x509_crt; +struct mbedtls_pk_context; + +EXTERN_C_BEGIN + + struct SolidSyslogMbedTlsStreamConfig + { + struct SolidSyslogStream* Transport; /* underlying byte stream — caller owns */ + SolidSyslogSleepFunction + Sleep; /* drives bounded handshake retry between WANT_READ/WANT_WRITE polls — required */ + struct mbedtls_ctr_drbg_context* Rng; /* seeded CTR-DRBG — caller owns */ + struct mbedtls_x509_crt* CaChain; /* trust anchors — caller owns */ + const char* ServerName; /* SNI + cert hostname check; NULL to skip */ + struct mbedtls_x509_crt* ClientCertChain; /* leaf (+ intermediates); NULL = no mTLS */ + struct mbedtls_pk_context* ClientKey; /* matching private key; NULL = no mTLS */ + }; + + struct SolidSyslogStream* SolidSyslogMbedTlsStream_Create(const struct SolidSyslogMbedTlsStreamConfig* config); + void SolidSyslogMbedTlsStream_Destroy(struct SolidSyslogStream * base); + +EXTERN_C_END + +#endif /* SOLIDSYSLOGMBEDTLSSTREAM_H */ diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c new file mode 100644 index 00000000..73da83b4 --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -0,0 +1,30 @@ +#include "SolidSyslogMbedTlsStream.h" + +#include "SolidSyslogMbedTlsStreamPrivate.h" +#include "SolidSyslogNullStream.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" + +static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base); + +void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config) +{ + /* Slice 1 (plumbing): no mbedTLS API calls yet. Wire the vtable to the + * shared NullStream so any caller exercising the handle gets safe no-ops + * until slice 2 lands the real Open/Send/Read/Close path. */ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + self->Base = *SolidSyslogNullStream_Get(); + self->Config = *config; +} + +void MbedTlsStream_Cleanup(struct SolidSyslogStream* base) +{ + /* Overwrite the abstract base with the shared NullStream vtable so + * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ + *base = *SolidSyslogNullStream_Get(); +} + +static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base) +{ + return (struct SolidSyslogMbedTlsStream*) base; +} diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h new file mode 100644 index 00000000..13df494d --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -0,0 +1,16 @@ +#ifndef SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H +#define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H + +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStreamDefinition.h" + +struct SolidSyslogMbedTlsStream +{ + struct SolidSyslogStream Base; + struct SolidSyslogMbedTlsStreamConfig Config; +}; + +void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config); +void MbedTlsStream_Cleanup(struct SolidSyslogStream* base); + +#endif /* SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H */ diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c new file mode 100644 index 00000000..0f87f79c --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c @@ -0,0 +1,72 @@ +#include "SolidSyslogMbedTlsStream.h" + +#include +#include + +#include "SolidSyslogError.h" +#include "SolidSyslogErrorMessages.h" +#include "SolidSyslogMbedTlsStreamPrivate.h" +#include "SolidSyslogNullStream.h" +#include "SolidSyslogPoolAllocator.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogTunables.h" + +struct SolidSyslogStream; + +static inline size_t MbedTlsStream_IndexFromHandle(const struct SolidSyslogStream* base); +static inline void MbedTlsStream_CleanupAtIndex(size_t index, void* context); + +static bool MbedTlsStream_InUse[SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE]; +static struct SolidSyslogMbedTlsStream MbedTlsStream_Pool[SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE]; +static struct SolidSyslogPoolAllocator MbedTlsStream_Allocator = { + MbedTlsStream_InUse, + SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE +}; + +struct SolidSyslogStream* SolidSyslogMbedTlsStream_Create(const struct SolidSyslogMbedTlsStreamConfig* config) +{ + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&MbedTlsStream_Allocator); + struct SolidSyslogStream* handle = SolidSyslogNullStream_Get(); + if (SolidSyslogPoolAllocator_IndexIsValid(&MbedTlsStream_Allocator, index)) + { + MbedTlsStream_Initialise(&MbedTlsStream_Pool[index].Base, config); + handle = &MbedTlsStream_Pool[index].Base; + } + else + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_POOL_EXHAUSTED); + } + return handle; +} + +void SolidSyslogMbedTlsStream_Destroy(struct SolidSyslogStream* base) +{ + size_t index = MbedTlsStream_IndexFromHandle(base); + bool released = + SolidSyslogPoolAllocator_IndexIsValid(&MbedTlsStream_Allocator, index) && + SolidSyslogPoolAllocator_FreeIfInUse(&MbedTlsStream_Allocator, index, MbedTlsStream_CleanupAtIndex, NULL); + if (!released) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_UNKNOWN_DESTROY); + } +} + +static inline size_t MbedTlsStream_IndexFromHandle(const struct SolidSyslogStream* base) +{ + size_t result = SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE; + for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE; poolIndex++) + { + if (base == &MbedTlsStream_Pool[poolIndex].Base) + { + result = poolIndex; + break; + } + } + return result; +} + +static inline void MbedTlsStream_CleanupAtIndex(size_t index, void* context) +{ + (void) context; + MbedTlsStream_Cleanup(&MbedTlsStream_Pool[index].Base); +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index c5a6356e..af37a982 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -16,6 +16,11 @@ if(DEFINED ENV{FATFS_PATH}) add_subdirectory(FatFs) endif() +if(SOLIDSYSLOG_MBEDTLS) + add_subdirectory(MbedTls) + add_subdirectory(MbedTlsIntegration) +endif() + set(TEST_SOURCES SolidSyslogTest.cpp SolidSyslogPoolTest.cpp @@ -246,6 +251,15 @@ foreach(fatfs_test_target IN ITEMS endif() endforeach() +foreach(mbedtls_test_target IN ITEMS + SolidSyslogMbedTlsStreamPoolTest + MbedTlsIntegrationTests +) + if(TARGET ${mbedtls_test_target}) + register_junit_test(${mbedtls_test_target}) + endif() +endforeach() + # Code coverage report using lcov/genhtml. # Requires the coverage preset: cmake --preset coverage && cmake --build --preset coverage --target coverage # lcov and genhtml must be installed (available in both container images). diff --git a/Tests/MbedTls/CMakeLists.txt b/Tests/MbedTls/CMakeLists.txt new file mode 100644 index 00000000..acf91afe --- /dev/null +++ b/Tests/MbedTls/CMakeLists.txt @@ -0,0 +1,32 @@ +# Tests/MbedTls — host-side unit tests for the mbedTLS adapter under +# Platform/MbedTls/Source/. Each test executable recompiles the adapter +# source under test with the test's own (or upstream's) mbedtls_config.h +# on the include path and links against $MBEDTLS_DIR/include — same shape +# as Tests/FatFs/ and Tests/FreeRtos/, see project_header_configured_platforms. +# +# Gated by SOLIDSYSLOG_MBEDTLS at the top-level CMakeLists (this CMakeLists +# is only entered when the option is ON), which itself defaults ON when +# the MBEDTLS_DIR environment variable is set. + +add_executable(SolidSyslogMbedTlsStreamPoolTest + SolidSyslogMbedTlsStreamPoolTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c +) + +target_link_libraries(SolidSyslogMbedTlsStreamPoolTest PRIVATE + ${PROJECT_NAME} + ConfigLockFake + ErrorHandlerFake + CppUTest + CppUTestExt +) + +target_include_directories(SolidSyslogMbedTlsStreamPoolTest PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface + ${CMAKE_SOURCE_DIR}/Core/Source +) + +add_test(NAME SolidSyslogMbedTlsStreamPoolTest COMMAND SolidSyslogMbedTlsStreamPoolTest) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp new file mode 100644 index 00000000..82e8e818 --- /dev/null +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp @@ -0,0 +1,180 @@ +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include "ConfigLockFake.h" +#include "ErrorHandlerFake.h" +#include "SolidSyslogErrorMessages.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogNullStream.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" +#include "SolidSyslogTunables.h" +} + +#include "TestUtils.h" + +using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER into scope for CALLED_FAKE + +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) -- macros preserve __FILE__/__LINE__ at the call site + +// Asserts handle is non-null and not one of the slots in pool. +#define CHECK_IS_FALLBACK(handle, pool) \ + do \ + { \ + CHECK_TEXT((handle) != nullptr, "Fallback handle was nullptr"); \ + for (auto* slot : (pool)) \ + { \ + CHECK_TEXT(slot != nullptr, "pool slot was nullptr (FillPool failed?)"); \ + CHECK_TEXT((handle) != slot, "Fallback handle collided with a pool slot"); \ + } \ + } while (0) + +// NOLINTEND(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsStreamPool) +{ + struct SolidSyslogMbedTlsStreamConfig config = {}; + // cppcheck-suppress constVariable -- assigned in test bodies; cppcheck does not model CppUTest lifecycle + struct SolidSyslogStream* pooled[SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE] = {}; + struct SolidSyslogStream* overflow = nullptr; + + void teardown() override + { + for (auto* handle : pooled) + { + if (handle != nullptr) + { + SolidSyslogMbedTlsStream_Destroy(handle); + } + } + // cppcheck-suppress knownConditionTrueFalse -- assigned in test bodies; cppcheck does not model CppUTest lifecycle + if (overflow != nullptr) + { + SolidSyslogMbedTlsStream_Destroy(overflow); + } + ConfigLockFake_Uninstall(); + ErrorHandlerFake_Uninstall(); + } + + void FillPool() + { + for (auto*& slot : pooled) + { + slot = SolidSyslogMbedTlsStream_Create(&config); + } + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsStreamPool, CreateReturnsHandleDistinctFromFallback) +{ + struct SolidSyslogStream* handle = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_TEXT(handle != nullptr, "Create returned nullptr"); + CHECK_TEXT(handle != SolidSyslogNullStream_Get(), "Create returned the NullStream fallback"); + + SolidSyslogMbedTlsStream_Destroy(handle); +} + +TEST(SolidSyslogMbedTlsStreamPool, FillingPoolThenOverflowReturnsDistinctFallback) +{ + FillPool(); + + overflow = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_IS_FALLBACK(overflow, pooled); +} + +TEST(SolidSyslogMbedTlsStreamPool, ExhaustedCreateReportsError) +{ + ErrorHandlerFake_Install(nullptr); + FillPool(); + + overflow = SolidSyslogMbedTlsStream_Create(&config); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); + STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_POOL_EXHAUSTED, ErrorHandlerFake_LastMessage()); +} + +TEST(SolidSyslogMbedTlsStreamPool, FallbackSendReturnsTrueToDropOnTheFloor) +{ + FillPool(); + overflow = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_TRUE(SolidSyslogStream_Send(overflow, "x", 1)); +} + +TEST(SolidSyslogMbedTlsStreamPool, CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot) +{ + ConfigLockFake_Install(); + + pooled[0] = SolidSyslogMbedTlsStream_Create(&config); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogMbedTlsStreamPool, CreateLocksOncePerSlotProbedWhenPoolIsFull) +{ + FillPool(); + ConfigLockFake_Install(); + + overflow = SolidSyslogMbedTlsStream_Create(&config); + + LONGS_EQUAL(SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE, ConfigLockFake_LockCallCount()); + LONGS_EQUAL(SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE, ConfigLockFake_UnlockCallCount()); +} + +TEST(SolidSyslogMbedTlsStreamPool, DestroyOfPooledHandleLocksOnce) +{ + pooled[0] = SolidSyslogMbedTlsStream_Create(&config); + ConfigLockFake_Install(); + + SolidSyslogMbedTlsStream_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogMbedTlsStreamPool, DestroyOfUnknownHandleDoesNotLock) +{ + ConfigLockFake_Install(); + struct SolidSyslogStream stranger = {}; + + SolidSyslogMbedTlsStream_Destroy(&stranger); + + CALLED_FAKE(ConfigLockFake_Lock, NEVER); + CALLED_FAKE(ConfigLockFake_Unlock, NEVER); +} + +TEST(SolidSyslogMbedTlsStreamPool, DestroyOfUnknownHandleReportsWarning) +{ + ErrorHandlerFake_Install(nullptr); + struct SolidSyslogStream stranger = {}; + + SolidSyslogMbedTlsStream_Destroy(&stranger); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); +} + +TEST(SolidSyslogMbedTlsStreamPool, DestroyOfStaleHandleReportsWarning) +{ + pooled[0] = SolidSyslogMbedTlsStream_Create(&config); + SolidSyslogMbedTlsStream_Destroy(pooled[0]); + ErrorHandlerFake_Install(nullptr); + + SolidSyslogMbedTlsStream_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_MBEDTLSSTREAM_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); +} diff --git a/Tests/MbedTls/main.cpp b/Tests/MbedTls/main.cpp new file mode 100644 index 00000000..abc587a2 --- /dev/null +++ b/Tests/MbedTls/main.cpp @@ -0,0 +1,6 @@ +#include "CppUTest/CommandLineTestRunner.h" + +int main(int argc, char* argv[]) +{ + return CommandLineTestRunner::RunAllTests(argc, argv); +} diff --git a/Tests/MbedTlsIntegration/CMakeLists.txt b/Tests/MbedTlsIntegration/CMakeLists.txt new file mode 100644 index 00000000..f7cb0151 --- /dev/null +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -0,0 +1,76 @@ +# mbedTLS-backed integration tests: links the real libmbedtls and (from +# slice 3 onward) drives an end-to-end handshake against an in-process TLS +# server. Separate binary from SolidSyslogTests because unit tests will +# link the future MbedTlsFake in place of libmbedtls — the two cannot share +# one executable without fake/real symbol collision. +# +# Builds mbedTLS from $MBEDTLS_DIR as a subproject. ENABLE_PROGRAMS is +# forced OFF here because we never want the upstream demo binaries (slow +# to build, no value to us). ENABLE_TESTING is left to the upstream default +# (OFF when added as a subproject). + +set(MBEDTLS_SOURCE_DIR "$ENV{MBEDTLS_DIR}") +if(NOT EXISTS "${MBEDTLS_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "Tests/MbedTlsIntegration requires MBEDTLS_DIR to point at an mbedTLS source tree; " + "got MBEDTLS_DIR='${MBEDTLS_SOURCE_DIR}' which has no CMakeLists.txt.") +endif() + +# Don't pollute our top-level option namespace with mbedTLS's options. +# MBEDTLS_FATAL_WARNINGS is force-OFF: upstream sources don't (and won't) +# pass our strict -Werror surface, and treating them as our responsibility +# would tie our build to an mbedTLS version. Same posture as FreeRTOS-Kernel, +# Plus-TCP, and FatFs — see project_header_configured_platforms. +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) + +# Belt-and-braces: our parent CMAKE_C_FLAGS adds -Werror=sign-conversion etc. +# that flow into add_subdirectory targets. Drop all warnings on the upstream +# libraries so a future mbedTLS bump doesn't break our build for unrelated +# diagnostic reasons. Same for clang-tidy: the parent build sets +# CMAKE__CLANG_TIDY under the `tidy` preset, which would apply our +# .clang-tidy rules to upstream code. Disable per-target. +foreach(_mbedtls_upstream_target IN ITEMS mbedtls mbedx509 mbedcrypto everest p256m) + if(TARGET ${_mbedtls_upstream_target}) + target_compile_options(${_mbedtls_upstream_target} PRIVATE -w) + set_target_properties(${_mbedtls_upstream_target} PROPERTIES + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" + ) + endif() +endforeach() + +add_executable(MbedTlsIntegrationTests + main.cpp + SolidSyslogMbedTlsStreamIntegrationTest.cpp +) + +target_link_libraries(MbedTlsIntegrationTests PRIVATE + ${PROJECT_NAME} + CppUTest + CppUTestExt + mbedtls + mbedx509 + mbedcrypto +) + +target_include_directories(MbedTlsIntegrationTests PRIVATE + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# cppcheck on this target trips on `#include ` because +# mbedTLS's build_info.h uses `#include MBEDTLS_USER_CONFIG_FILE` (a macro) +# which cppcheck's preprocessor cannot resolve. The integration test's whole +# purpose is to exercise real libmbedtls end-to-end, so the static-analysis +# value on this TU is low — disable cppcheck on the target. +set_target_properties(MbedTlsIntegrationTests PROPERTIES + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +add_test(NAME MbedTlsIntegrationTests COMMAND MbedTlsIntegrationTests) diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp new file mode 100644 index 00000000..c1395a8a --- /dev/null +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -0,0 +1,23 @@ +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include +} + +// Slice 1 (plumbing) skeleton: proves the integration-test binary links +// against real libmbedtls built from $MBEDTLS_DIR. The handshake-driving +// in-process TLS server lands in slice 3 alongside the certificate +// validation tests. + +TEST_GROUP(SolidSyslogMbedTlsStreamIntegration){}; + +TEST(SolidSyslogMbedTlsStreamIntegration, BinaryLinksAgainstRealLibMbedTls) +{ + /* mbedtls_version_get_number() is a constant, side-effect-free symbol + * present in every mbedTLS build — a successful link plus a return + * value matching the expected major version (3.x) confirms the + * integration scaffold pulls in the real library, not a fake. */ + const unsigned int major = (mbedtls_version_get_number() >> 24) & 0xFFU; + LONGS_EQUAL(3, major); +} diff --git a/Tests/MbedTlsIntegration/main.cpp b/Tests/MbedTlsIntegration/main.cpp new file mode 100644 index 00000000..abc587a2 --- /dev/null +++ b/Tests/MbedTlsIntegration/main.cpp @@ -0,0 +1,6 @@ +#include "CppUTest/CommandLineTestRunner.h" + +int main(int argc, char* argv[]) +{ + return CommandLineTestRunner::RunAllTests(argc, argv); +} From 9aa22f62f073c60440436245e1315da13e50f24c Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 06:59:37 +0000 Subject: [PATCH 02/13] feat: S08.07 slice 2 MbedTlsStream walking-skeleton happy path Drives the vtable end-to-end against MbedTlsFake (link-time interposition). Open builds the ssl_config (init + defaults for CLIENT/STREAM/DEFAULT preset), inits and setups the ssl_context, wires the BIO bridge via mbedtls_ssl_set_bio (p_bio = self, send/recv callbacks delegate to the injected transport, no recv_timeout), and drives the handshake. Send is mbedtls_ssl_write; Read is mbedtls_ssl_read with WANT_READ -> 0 and other errors -> -1. Close issues close_notify, frees ssl_context and ssl_config, then closes the transport. The deliberately-deferred slices are visible in the production code: no conf_authmode / conf_ca_chain / set_hostname yet (slice 3), no bounded WANT_READ/WANT_WRITE handshake retry (slice 4), no conf_own_cert / mTLS (slice 5). Tests: 22 vtable tests in Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp (separate binary from the pool tests because each pins different concerns). The shared adapter source is compiled into both test exes via the existing include path; MbedTlsFakes is linked PUBLICly to bring $MBEDTLS_DIR/include along. Tests/Support/MbedTlsFake mirrors OpenSslFake: per-symbol call counters, captured-arg accessors, settable return values where the production code's branch depends on them. cppcheck disabled on every TU that pulls in because mbedTLS's MBEDTLS_USER_CONFIG_FILE macro indirection trips its preprocessor (same posture as OpenSSL upstream sources). Pre-commit (freertos-host container): format clean, tidy clean, cppcheck clean, sanitize green on both MbedTls binaries. Story: #272. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 98 ++++- .../Source/SolidSyslogMbedTlsStreamPrivate.h | 4 + Tests/CMakeLists.txt | 1 + Tests/MbedTls/CMakeLists.txt | 44 ++ .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 285 +++++++++++++ Tests/Support/CMakeLists.txt | 22 + Tests/Support/MbedTlsFake.c | 395 ++++++++++++++++++ Tests/Support/MbedTlsFake.h | 77 ++++ 8 files changed, 923 insertions(+), 3 deletions(-) create mode 100644 Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp create mode 100644 Tests/Support/MbedTlsFake.c create mode 100644 Tests/Support/MbedTlsFake.h diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 73da83b4..0a56e74c 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -1,19 +1,34 @@ #include "SolidSyslogMbedTlsStream.h" +#include +#include +#include + #include "SolidSyslogMbedTlsStreamPrivate.h" #include "SolidSyslogNullStream.h" #include "SolidSyslogStream.h" #include "SolidSyslogStreamDefinition.h" +struct SolidSyslogAddress; + static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base); +static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); +static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size); +static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size); +static inline void MbedTlsStream_Close(struct SolidSyslogStream* base); +static int MbedTlsStream_BioSend(void* ctx, const unsigned char* buf, size_t len); +static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len); void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config) { - /* Slice 1 (plumbing): no mbedTLS API calls yet. Wire the vtable to the - * shared NullStream so any caller exercising the handle gets safe no-ops - * until slice 2 lands the real Open/Send/Read/Close path. */ struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + /* Read / Close inherit NullStream defaults until slice-2 tests force + * their real implementations. */ self->Base = *SolidSyslogNullStream_Get(); + self->Base.Open = MbedTlsStream_Open; + self->Base.Send = MbedTlsStream_Send; + self->Base.Read = MbedTlsStream_Read; + self->Base.Close = MbedTlsStream_Close; self->Config = *config; } @@ -28,3 +43,80 @@ static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct { return (struct SolidSyslogMbedTlsStream*) base; } + +static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr) +{ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + bool ok = SolidSyslogStream_Open(self->Config.Transport, addr); + if (ok) + { + mbedtls_ssl_config_init(&self->SslConfig); + ok = mbedtls_ssl_config_defaults( + &self->SslConfig, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ) == 0; + } + if (ok) + { + mbedtls_ssl_init(&self->SslContext); + ok = mbedtls_ssl_setup(&self->SslContext, &self->SslConfig) == 0; + } + if (ok) + { + mbedtls_ssl_set_bio(&self->SslContext, self, MbedTlsStream_BioSend, MbedTlsStream_BioRecv, NULL); + ok = mbedtls_ssl_handshake(&self->SslContext) == 0; + } + return ok; +} + +static int MbedTlsStream_BioSend(void* ctx, const unsigned char* buf, size_t len) +{ + struct SolidSyslogMbedTlsStream* self = (struct SolidSyslogMbedTlsStream*) ctx; + return SolidSyslogStream_Send(self->Config.Transport, buf, len) ? (int) len : -1; +} + +static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len) +{ + struct SolidSyslogMbedTlsStream* self = (struct SolidSyslogMbedTlsStream*) ctx; + SolidSyslogSsize n = SolidSyslogStream_Read(self->Config.Transport, buf, len); + int result = -1; + if (n > 0) + { + result = (int) n; + } + return result; +} + +static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size) +{ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + int rc = mbedtls_ssl_write(&self->SslContext, (const unsigned char*) buffer, size); + return (rc > 0) && ((size_t) rc == size); +} + +static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size) +{ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + int rc = mbedtls_ssl_read(&self->SslContext, (unsigned char*) buffer, size); + SolidSyslogSsize result = -1; + if (rc > 0) + { + result = (SolidSyslogSsize) rc; + } + else if (rc == MBEDTLS_ERR_SSL_WANT_READ) + { + result = 0; + } + return result; +} + +static inline void MbedTlsStream_Close(struct SolidSyslogStream* base) +{ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + (void) mbedtls_ssl_close_notify(&self->SslContext); + mbedtls_ssl_free(&self->SslContext); + mbedtls_ssl_config_free(&self->SslConfig); + SolidSyslogStream_Close(self->Config.Transport); +} diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h index 13df494d..e6661a89 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -1,6 +1,8 @@ #ifndef SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H #define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H +#include + #include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogStreamDefinition.h" @@ -8,6 +10,8 @@ struct SolidSyslogMbedTlsStream { struct SolidSyslogStream Base; struct SolidSyslogMbedTlsStreamConfig Config; + mbedtls_ssl_config SslConfig; + mbedtls_ssl_context SslContext; }; void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config); diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index af37a982..9917155a 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -253,6 +253,7 @@ endforeach() foreach(mbedtls_test_target IN ITEMS SolidSyslogMbedTlsStreamPoolTest + SolidSyslogMbedTlsStreamTest MbedTlsIntegrationTests ) if(TARGET ${mbedtls_test_target}) diff --git a/Tests/MbedTls/CMakeLists.txt b/Tests/MbedTls/CMakeLists.txt index acf91afe..b45e140c 100644 --- a/Tests/MbedTls/CMakeLists.txt +++ b/Tests/MbedTls/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(SolidSyslogMbedTlsStreamPoolTest PRIVATE ${PROJECT_NAME} ConfigLockFake ErrorHandlerFake + MbedTlsFakes CppUTest CppUTestExt ) @@ -29,4 +30,47 @@ target_include_directories(SolidSyslogMbedTlsStreamPoolTest PRIVATE ${CMAKE_SOURCE_DIR}/Core/Source ) +# cppcheck cannot resolve mbedTLS's `#include MBEDTLS_USER_CONFIG_FILE` +# macro indirection in build_info.h, so it errors on every TU that pulls +# in . Both test exes pull mbedtls headers via the production +# adapter source and the MbedTlsFakes link; disable cppcheck per-target, +# same as MbedTlsIntegrationTests. +set_target_properties(SolidSyslogMbedTlsStreamPoolTest PROPERTIES + C_CPPCHECK "" + CXX_CPPCHECK "" +) + add_test(NAME SolidSyslogMbedTlsStreamPoolTest COMMAND SolidSyslogMbedTlsStreamPoolTest) + +# Slice-2 onward: vtable behaviour driven against MbedTlsFake (link-time +# interposition of libmbedtls — separate binary from the integration test). +add_executable(SolidSyslogMbedTlsStreamTest + SolidSyslogMbedTlsStreamTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c + ${CMAKE_SOURCE_DIR}/Tests/StreamFake.c +) + +target_link_libraries(SolidSyslogMbedTlsStreamTest PRIVATE + ${PROJECT_NAME} + ConfigLockFake + ErrorHandlerFake + MbedTlsFakes + CppUTest + CppUTestExt +) + +target_include_directories(SolidSyslogMbedTlsStreamTest PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests +) + +set_target_properties(SolidSyslogMbedTlsStreamTest PROPERTIES + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +add_test(NAME SolidSyslogMbedTlsStreamTest COMMAND SolidSyslogMbedTlsStreamTest) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp new file mode 100644 index 00000000..917607c6 --- /dev/null +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -0,0 +1,285 @@ +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include + +#include "MbedTlsFake.h" +#include "SolidSyslogAddress.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" +#include "StreamFake.h" +} + +namespace +{ +void NoOpSleep(int milliseconds) +{ + (void) milliseconds; +} +} // namespace + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsStream) +{ + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogStream* handle = nullptr; + struct SolidSyslogMbedTlsStreamConfig config = {}; + + void setup() override + { + MbedTlsFake_Reset(); + transport = StreamFake_Create(); + config.Transport = transport; + config.Sleep = NoOpSleep; + handle = SolidSyslogMbedTlsStream_Create(&config); + } + + void teardown() override + { + SolidSyslogMbedTlsStream_Destroy(handle); + StreamFake_Destroy(transport); + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsStream, OpenDelegatesToInjectedTransport) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, StreamFake_OpenCallCount(transport)); + POINTERS_EQUAL(addr, StreamFake_LastOpenAddr(transport)); +} + +TEST(SolidSyslogMbedTlsStream, OpenInitialisesSslConfig) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfigInitCallCount()); +} + +TEST(SolidSyslogMbedTlsStream, OpenAppliesClientStreamDefaultsToSslConfig) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfigDefaultsCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfigDefaultsConfigArg()); + LONGS_EQUAL(MBEDTLS_SSL_IS_CLIENT, MbedTlsFake_LastSslConfigDefaultsEndpoint()); + LONGS_EQUAL(MBEDTLS_SSL_TRANSPORT_STREAM, MbedTlsFake_LastSslConfigDefaultsTransport()); + LONGS_EQUAL(MBEDTLS_SSL_PRESET_DEFAULT, MbedTlsFake_LastSslConfigDefaultsPreset()); +} + +TEST(SolidSyslogMbedTlsStream, OpenInitialisesSslContext) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslInitCallCount()); +} + +TEST(SolidSyslogMbedTlsStream, OpenSetupBindsSslContextToSslConfig) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslSetupCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslSetupContextArg()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslSetupConfigArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslSetBioCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslSetBioContextArg()); + CHECK(MbedTlsFake_LastSslSetBioPBioArg() != nullptr); + CHECK(MbedTlsFake_LastSslSetBioSendCallback() != nullptr); + CHECK(MbedTlsFake_LastSslSetBioRecvCallback() != nullptr); +} + +TEST(SolidSyslogMbedTlsStream, OpenDrivesHandshakeOnTheSslContext) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslHandshakeCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslHandshakeArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenReturnsTrueWhenHandshakeSucceeds) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + MbedTlsFake_SetSslHandshakeReturn(0); + + CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); +} + +TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + MbedTlsFake_SetSslHandshakeReturn(-1); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); +} + +TEST(SolidSyslogMbedTlsStream, SendForwardsBufferToSslWrite) +{ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + + SolidSyslogStream_Send(handle, payload, sizeof(payload)); + + LONGS_EQUAL(1, MbedTlsFake_SslWriteCallCount()); + POINTERS_EQUAL(payload, MbedTlsFake_LastSslWriteBufArg()); + LONGS_EQUAL(sizeof(payload), MbedTlsFake_LastSslWriteLenArg()); +} + +TEST(SolidSyslogMbedTlsStream, SendReturnsTrueWhenSslWriteWritesAllBytes) +{ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + MbedTlsFake_SetSslWriteReturn((int) sizeof(payload)); + + CHECK_TRUE(SolidSyslogStream_Send(handle, payload, sizeof(payload))); +} + +TEST(SolidSyslogMbedTlsStream, SendReturnsFalseWhenSslWriteWritesPartial) +{ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + MbedTlsFake_SetSslWriteReturn(1); + + CHECK_FALSE(SolidSyslogStream_Send(handle, payload, sizeof(payload))); +} + +TEST(SolidSyslogMbedTlsStream, SendReturnsFalseWhenSslWriteFails) +{ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + MbedTlsFake_SetSslWriteReturn(-1); + + CHECK_FALSE(SolidSyslogStream_Send(handle, payload, sizeof(payload))); +} + +TEST(SolidSyslogMbedTlsStream, ReadForwardsBufferToSslRead) +{ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(3); + + SolidSyslogStream_Read(handle, buffer, sizeof(buffer)); + + LONGS_EQUAL(1, MbedTlsFake_SslReadCallCount()); + POINTERS_EQUAL(buffer, MbedTlsFake_LastSslReadBufArg()); + LONGS_EQUAL(sizeof(buffer), MbedTlsFake_LastSslReadLenArg()); +} + +TEST(SolidSyslogMbedTlsStream, ReadReturnsByteCountWhenSslReadReturnsPositive) +{ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(5); + + LONGS_EQUAL(5, SolidSyslogStream_Read(handle, buffer, sizeof(buffer))); +} + +TEST(SolidSyslogMbedTlsStream, ReadReturnsZeroOnWantRead) +{ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(MBEDTLS_ERR_SSL_WANT_READ); + + LONGS_EQUAL(0, SolidSyslogStream_Read(handle, buffer, sizeof(buffer))); +} + +TEST(SolidSyslogMbedTlsStream, ReadReturnsNegativeOnSslReadError) +{ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(-1); + + CHECK(SolidSyslogStream_Read(handle, buffer, sizeof(buffer)) < 0); +} + +TEST(SolidSyslogMbedTlsStream, CloseSendsSslCloseNotifyOnTheSslContextFromOpen) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + + SolidSyslogStream_Close(handle); + + LONGS_EQUAL(1, MbedTlsFake_SslCloseNotifyCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslCloseNotifyArg()); +} + +TEST(SolidSyslogMbedTlsStream, CloseFreesSslContextAndSslConfigFromOpen) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + + SolidSyslogStream_Close(handle); + + LONGS_EQUAL(1, MbedTlsFake_SslFreeCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslFreeArg()); + LONGS_EQUAL(1, MbedTlsFake_SslConfigFreeCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfigFreeArg()); +} + +TEST(SolidSyslogMbedTlsStream, CloseDelegatesToInjectedTransport) +{ + SolidSyslogStream_Close(handle); + + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); +} + +TEST(SolidSyslogMbedTlsStream, BioSendCallbackForwardsBufferToTransport) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + auto* bioSend = MbedTlsFake_LastSslSetBioSendCallback(); + void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); + const unsigned char payload[] = {0xAA, 0xBB, 0xCC}; + + int rc = bioSend(bioContext, payload, sizeof(payload)); + + LONGS_EQUAL((int) sizeof(payload), rc); + LONGS_EQUAL(1, StreamFake_SendCallCount(transport)); + POINTERS_EQUAL(payload, StreamFake_LastSendBuf(transport)); + LONGS_EQUAL(sizeof(payload), StreamFake_LastSendSize(transport)); +} + +TEST(SolidSyslogMbedTlsStream, BioRecvCallbackForwardsBufferToTransport) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); + void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); + unsigned char buffer[16]; + StreamFake_SetReadReturn(transport, 4); + + int rc = bioRecv(bioContext, buffer, sizeof(buffer)); + + LONGS_EQUAL(4, rc); + LONGS_EQUAL(1, StreamFake_ReadCallCount(transport)); + POINTERS_EQUAL(buffer, StreamFake_LastReadBuf(transport)); + LONGS_EQUAL(sizeof(buffer), StreamFake_LastReadSize(transport)); +} diff --git a/Tests/Support/CMakeLists.txt b/Tests/Support/CMakeLists.txt index 05cabb95..85925c9e 100644 --- a/Tests/Support/CMakeLists.txt +++ b/Tests/Support/CMakeLists.txt @@ -41,3 +41,25 @@ if(SOLIDSYSLOG_OPENSSL) target_link_libraries(OpenSslFakes PUBLIC ${PROJECT_NAME}) endif() + +# mbedTLS fake — same shape as OpenSslFakes. Headers come from $MBEDTLS_DIR; +# we never link upstream libmbedtls into a target that pulls in this fake. +if(SOLIDSYSLOG_MBEDTLS) + add_library(MbedTlsFakes STATIC MbedTlsFake.c) + + target_include_directories(MbedTlsFakes PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + $ENV{MBEDTLS_DIR}/include + ) + + target_link_libraries(MbedTlsFakes PUBLIC ${PROJECT_NAME}) + + # cppcheck cannot resolve mbedTLS's `#include MBEDTLS_USER_CONFIG_FILE` + # macro indirection in build_info.h, so it errors on every TU that pulls + # in . The fake's whole point is to interpose mbedTLS + # symbols — static-analysis value here is minimal. Disable per-target. + set_target_properties(MbedTlsFakes PROPERTIES + C_CPPCHECK "" + CXX_CPPCHECK "" + ) +endif() diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c new file mode 100644 index 00000000..ba25d78c --- /dev/null +++ b/Tests/Support/MbedTlsFake.c @@ -0,0 +1,395 @@ +#include "MbedTlsFake.h" + +#include +#include +#include + +/* ------------------------------------------------------------------------- + * Captured state — one section per mbedTLS API call. Tests read these via + * accessors below; production reaches libmbedtls through the link-interposed + * functions at the bottom of the file. + * ------------------------------------------------------------------------- */ + +/* mbedtls_ssl_config_init */ +static int sslConfigInitCallCount; +static mbedtls_ssl_config* lastSslConfigInitArg; + +/* mbedtls_ssl_config_defaults */ +static int sslConfigDefaultsCallCount; +static mbedtls_ssl_config* lastSslConfigDefaultsConfigArg; +static int lastSslConfigDefaultsEndpoint; +static int lastSslConfigDefaultsTransport; +static int lastSslConfigDefaultsPreset; + +/* mbedtls_ssl_init */ +static int sslInitCallCount; +static mbedtls_ssl_context* lastSslInitArg; + +/* mbedtls_ssl_setup */ +static int sslSetupCallCount; +static mbedtls_ssl_context* lastSslSetupContextArg; +static const mbedtls_ssl_config* lastSslSetupConfigArg; + +/* mbedtls_ssl_set_bio */ +static int sslSetBioCallCount; +static mbedtls_ssl_context* lastSslSetBioContextArg; +static void* lastSslSetBioPBioArg; +static mbedtls_ssl_send_t* lastSslSetBioSendCallback; +static mbedtls_ssl_recv_t* lastSslSetBioRecvCallback; +static mbedtls_ssl_recv_timeout_t* lastSslSetBioRecvTimeoutCallback; + +/* mbedtls_ssl_handshake */ +static int sslHandshakeCallCount; +static mbedtls_ssl_context* lastSslHandshakeArg; +static int sslHandshakeReturn; + +/* mbedtls_ssl_write */ +static int sslWriteCallCount; +static mbedtls_ssl_context* lastSslWriteContextArg; +static const unsigned char* lastSslWriteBufArg; +static size_t lastSslWriteLenArg; +static int sslWriteReturn; +static bool sslWriteReturnSet; + +/* mbedtls_ssl_read */ +static int sslReadCallCount; +static mbedtls_ssl_context* lastSslReadContextArg; +static unsigned char* lastSslReadBufArg; +static size_t lastSslReadLenArg; +static int sslReadReturn; + +/* mbedtls_ssl_close_notify */ +static int sslCloseNotifyCallCount; +static mbedtls_ssl_context* lastSslCloseNotifyArg; + +/* mbedtls_ssl_free */ +static int sslFreeCallCount; +static mbedtls_ssl_context* lastSslFreeArg; + +/* mbedtls_ssl_config_free */ +static int sslConfigFreeCallCount; +static mbedtls_ssl_config* lastSslConfigFreeArg; + +/* ------------------------------------------------------------------------- + * Test accessors. + * ------------------------------------------------------------------------- */ + +void MbedTlsFake_Reset(void) +{ + sslConfigInitCallCount = 0; + lastSslConfigInitArg = NULL; + sslConfigDefaultsCallCount = 0; + lastSslConfigDefaultsConfigArg = NULL; + lastSslConfigDefaultsEndpoint = 0; + lastSslConfigDefaultsTransport = 0; + lastSslConfigDefaultsPreset = 0; + sslInitCallCount = 0; + lastSslInitArg = NULL; + sslSetupCallCount = 0; + lastSslSetupContextArg = NULL; + lastSslSetupConfigArg = NULL; + sslSetBioCallCount = 0; + lastSslSetBioContextArg = NULL; + lastSslSetBioPBioArg = NULL; + lastSslSetBioSendCallback = NULL; + lastSslSetBioRecvCallback = NULL; + lastSslSetBioRecvTimeoutCallback = NULL; + sslHandshakeCallCount = 0; + lastSslHandshakeArg = NULL; + sslHandshakeReturn = 0; + sslWriteCallCount = 0; + lastSslWriteContextArg = NULL; + lastSslWriteBufArg = NULL; + lastSslWriteLenArg = 0; + sslWriteReturn = 0; + sslWriteReturnSet = false; + sslReadCallCount = 0; + lastSslReadContextArg = NULL; + lastSslReadBufArg = NULL; + lastSslReadLenArg = 0; + sslReadReturn = 0; + sslCloseNotifyCallCount = 0; + lastSslCloseNotifyArg = NULL; + sslFreeCallCount = 0; + lastSslFreeArg = NULL; + sslConfigFreeCallCount = 0; + lastSslConfigFreeArg = NULL; +} + +int MbedTlsFake_SslConfigInitCallCount(void) +{ + return sslConfigInitCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfigInitArg(void) +{ + return lastSslConfigInitArg; +} + +int MbedTlsFake_SslConfigDefaultsCallCount(void) +{ + return sslConfigDefaultsCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfigDefaultsConfigArg(void) +{ + return lastSslConfigDefaultsConfigArg; +} + +int MbedTlsFake_LastSslConfigDefaultsEndpoint(void) +{ + return lastSslConfigDefaultsEndpoint; +} + +int MbedTlsFake_LastSslConfigDefaultsTransport(void) +{ + return lastSslConfigDefaultsTransport; +} + +int MbedTlsFake_LastSslConfigDefaultsPreset(void) +{ + return lastSslConfigDefaultsPreset; +} + +int MbedTlsFake_SslInitCallCount(void) +{ + return sslInitCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslInitArg(void) +{ + return lastSslInitArg; +} + +int MbedTlsFake_SslSetupCallCount(void) +{ + return sslSetupCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslSetupContextArg(void) +{ + return lastSslSetupContextArg; +} + +const mbedtls_ssl_config* MbedTlsFake_LastSslSetupConfigArg(void) +{ + return lastSslSetupConfigArg; +} + +int MbedTlsFake_SslSetBioCallCount(void) +{ + return sslSetBioCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslSetBioContextArg(void) +{ + return lastSslSetBioContextArg; +} + +void* MbedTlsFake_LastSslSetBioPBioArg(void) +{ + return lastSslSetBioPBioArg; +} + +mbedtls_ssl_send_t* MbedTlsFake_LastSslSetBioSendCallback(void) +{ + return lastSslSetBioSendCallback; +} + +mbedtls_ssl_recv_t* MbedTlsFake_LastSslSetBioRecvCallback(void) +{ + return lastSslSetBioRecvCallback; +} + +int MbedTlsFake_SslHandshakeCallCount(void) +{ + return sslHandshakeCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslHandshakeArg(void) +{ + return lastSslHandshakeArg; +} + +void MbedTlsFake_SetSslHandshakeReturn(int value) +{ + sslHandshakeReturn = value; +} + +int MbedTlsFake_SslWriteCallCount(void) +{ + return sslWriteCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslWriteContextArg(void) +{ + return lastSslWriteContextArg; +} + +const unsigned char* MbedTlsFake_LastSslWriteBufArg(void) +{ + return lastSslWriteBufArg; +} + +size_t MbedTlsFake_LastSslWriteLenArg(void) +{ + return lastSslWriteLenArg; +} + +void MbedTlsFake_SetSslWriteReturn(int value) +{ + sslWriteReturn = value; + sslWriteReturnSet = true; +} + +int MbedTlsFake_SslReadCallCount(void) +{ + return sslReadCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslReadContextArg(void) +{ + return lastSslReadContextArg; +} + +unsigned char* MbedTlsFake_LastSslReadBufArg(void) +{ + return lastSslReadBufArg; +} + +size_t MbedTlsFake_LastSslReadLenArg(void) +{ + return lastSslReadLenArg; +} + +void MbedTlsFake_SetSslReadReturn(int value) +{ + sslReadReturn = value; +} + +int MbedTlsFake_SslCloseNotifyCallCount(void) +{ + return sslCloseNotifyCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslCloseNotifyArg(void) +{ + return lastSslCloseNotifyArg; +} + +int MbedTlsFake_SslFreeCallCount(void) +{ + return sslFreeCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslFreeArg(void) +{ + return lastSslFreeArg; +} + +int MbedTlsFake_SslConfigFreeCallCount(void) +{ + return sslConfigFreeCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfigFreeArg(void) +{ + return lastSslConfigFreeArg; +} + +/* ------------------------------------------------------------------------- + * Link-interposed mbedTLS symbols. The test executable does not link + * libmbedtls; the production code's calls to mbedtls_* resolve here. + * ------------------------------------------------------------------------- */ + +void mbedtls_ssl_config_init(mbedtls_ssl_config* conf) +{ + sslConfigInitCallCount++; + lastSslConfigInitArg = conf; +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by mbedTLS API +int mbedtls_ssl_config_defaults(mbedtls_ssl_config* conf, int endpoint, int transport, int preset) +{ + sslConfigDefaultsCallCount++; + lastSslConfigDefaultsConfigArg = conf; + lastSslConfigDefaultsEndpoint = endpoint; + lastSslConfigDefaultsTransport = transport; + lastSslConfigDefaultsPreset = preset; + return 0; +} + +void mbedtls_ssl_init(mbedtls_ssl_context* ssl) +{ + sslInitCallCount++; + lastSslInitArg = ssl; +} + +int mbedtls_ssl_setup(mbedtls_ssl_context* ssl, const mbedtls_ssl_config* conf) +{ + sslSetupCallCount++; + lastSslSetupContextArg = ssl; + lastSslSetupConfigArg = conf; + return 0; +} + +void mbedtls_ssl_set_bio( + mbedtls_ssl_context* ssl, + void* p_bio, + mbedtls_ssl_send_t* f_send, + mbedtls_ssl_recv_t* f_recv, + mbedtls_ssl_recv_timeout_t* f_recv_timeout +) +{ + sslSetBioCallCount++; + lastSslSetBioContextArg = ssl; + lastSslSetBioPBioArg = p_bio; + lastSslSetBioSendCallback = f_send; + lastSslSetBioRecvCallback = f_recv; + lastSslSetBioRecvTimeoutCallback = f_recv_timeout; +} + +int mbedtls_ssl_handshake(mbedtls_ssl_context* ssl) +{ + sslHandshakeCallCount++; + lastSslHandshakeArg = ssl; + return sslHandshakeReturn; +} + +int mbedtls_ssl_write(mbedtls_ssl_context* ssl, const unsigned char* buf, size_t len) +{ + sslWriteCallCount++; + lastSslWriteContextArg = ssl; + lastSslWriteBufArg = buf; + lastSslWriteLenArg = len; + /* Default: write succeeds fully (returns len). Tests can override via SetSslWriteReturn. */ + return sslWriteReturnSet ? sslWriteReturn : (int) len; +} + +int mbedtls_ssl_read(mbedtls_ssl_context* ssl, unsigned char* buf, size_t len) +{ + sslReadCallCount++; + lastSslReadContextArg = ssl; + lastSslReadBufArg = buf; + lastSslReadLenArg = len; + return sslReadReturn; +} + +int mbedtls_ssl_close_notify(mbedtls_ssl_context* ssl) +{ + sslCloseNotifyCallCount++; + lastSslCloseNotifyArg = ssl; + return 0; +} + +void mbedtls_ssl_free(mbedtls_ssl_context* ssl) +{ + sslFreeCallCount++; + lastSslFreeArg = ssl; +} + +void mbedtls_ssl_config_free(mbedtls_ssl_config* conf) +{ + sslConfigFreeCallCount++; + lastSslConfigFreeArg = conf; +} diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h new file mode 100644 index 00000000..35ace4ea --- /dev/null +++ b/Tests/Support/MbedTlsFake.h @@ -0,0 +1,77 @@ +#ifndef MBEDTLSFAKE_H +#define MBEDTLSFAKE_H + +#include + +#include "ExternC.h" + +struct mbedtls_ssl_config; +struct mbedtls_ssl_context; + +EXTERN_C_BEGIN + + /* Resets every counter and captured-arg field to its initial state. + * Call from TEST_GROUP::setup() so each test starts from a clean slate. */ + void MbedTlsFake_Reset(void); + + /* mbedtls_ssl_config_init */ + int MbedTlsFake_SslConfigInitCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfigInitArg(void); + + /* mbedtls_ssl_config_defaults */ + int MbedTlsFake_SslConfigDefaultsCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfigDefaultsConfigArg(void); + int MbedTlsFake_LastSslConfigDefaultsEndpoint(void); + int MbedTlsFake_LastSslConfigDefaultsTransport(void); + int MbedTlsFake_LastSslConfigDefaultsPreset(void); + + /* mbedtls_ssl_init */ + int MbedTlsFake_SslInitCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslInitArg(void); + + /* mbedtls_ssl_setup */ + int MbedTlsFake_SslSetupCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslSetupContextArg(void); + const struct mbedtls_ssl_config* MbedTlsFake_LastSslSetupConfigArg(void); + + /* mbedtls_ssl_set_bio */ + int MbedTlsFake_SslSetBioCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslSetBioContextArg(void); + void* MbedTlsFake_LastSslSetBioPBioArg(void); + int (*MbedTlsFake_LastSslSetBioSendCallback(void))(void*, const unsigned char*, size_t); + int (*MbedTlsFake_LastSslSetBioRecvCallback(void))(void*, unsigned char*, size_t); + + /* mbedtls_ssl_handshake */ + int MbedTlsFake_SslHandshakeCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslHandshakeArg(void); + void MbedTlsFake_SetSslHandshakeReturn(int value); + + /* mbedtls_ssl_write */ + int MbedTlsFake_SslWriteCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslWriteContextArg(void); + const unsigned char* MbedTlsFake_LastSslWriteBufArg(void); + size_t MbedTlsFake_LastSslWriteLenArg(void); + void MbedTlsFake_SetSslWriteReturn(int value); + + /* mbedtls_ssl_read */ + int MbedTlsFake_SslReadCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslReadContextArg(void); + unsigned char* MbedTlsFake_LastSslReadBufArg(void); + size_t MbedTlsFake_LastSslReadLenArg(void); + void MbedTlsFake_SetSslReadReturn(int value); + + /* mbedtls_ssl_close_notify */ + int MbedTlsFake_SslCloseNotifyCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslCloseNotifyArg(void); + + /* mbedtls_ssl_free */ + int MbedTlsFake_SslFreeCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslFreeArg(void); + + /* mbedtls_ssl_config_free */ + int MbedTlsFake_SslConfigFreeCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfigFreeArg(void); + +EXTERN_C_END + +#endif /* MBEDTLSFAKE_H */ From 29a6b36e92a0e629698b5d34ce0bf86234916c14 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 08:58:42 +0000 Subject: [PATCH 03/13] feat: S08.07 slice 3 MbedTlsStream cert-validation policy + integration scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open's chain now applies the certificate-validation policy on the per-instance ssl_config (project_mbedtls_coexistence_contract holds — none of these calls leak to any other ssl_config the integrator might own): - mbedtls_ssl_conf_authmode(MBEDTLS_SSL_VERIFY_REQUIRED) - mbedtls_ssl_conf_ca_chain(config.CaChain, /* CRL */ NULL) - mbedtls_ssl_conf_rng(mbedtls_ctr_drbg_random, config.Rng) - mbedtls_ssl_set_hostname(config.ServerName) when ServerName != NULL Five new unit tests (27 total in SolidSyslogMbedTlsStreamTest, all green) plus a small fixture refactor (ReCreateHandleWithUpdatedConfig) so tests that mutate Config can re-Create after setup(). Integration scaffold under Tests/MbedTlsIntegration/: - SocketStream.{c,h}: minimal raw-fd SolidSyslogStream over one end of a socketpair; client-side transport for the integration test. - MbedTlsTestCert.{c,h}: in-memory CA + server-cert + RSA-2048 key generation via mbedtls_x509write_crt_*; certs are immediately usable by mbedtls_ssl_conf_ca_chain / _conf_own_cert without going via PEM. - MbedTlsTestServer.{c,h}: server-side mbedTLS driven on a pthread; runs just the handshake step and exits (slice 4+ extends it with read-loop behaviour if a test needs to assert on delivered bytes). Happy-path integration test passes end-to-end against real libmbedtls (handshake completes when the server cert chains to the trusted CA and the SAN matches ServerName). The two negative scenarios — wrong-CA and mismatched-hostname — are landed as IGNORE_TEST with a TODO; the client correctly returns the verify error but the server thread stays blocked during the handshake response (likely the TLS-1.3-vs-1.2 sequencing the OpenSSL TlsTestServer pins around). Re-enable in a follow-up by capping the server's max version to TLS 1.2 or by feeding the worker a deadline. Also adds .vscode/c_cpp_properties.json pointing the C/C++ extension at build/debug/compile_commands.json so IntelliSense resolves correctly in the devcontainer (no more "cannot open source file" squiggles). Pre-commit (freertos-host container): format clean across MbedTls files, all 27 unit tests green, integration binary runs 2 / ignores 2 / 0 failures. Story: #272. Co-Authored-By: Claude Opus 4.7 (1M context) --- .vscode/c_cpp_properties.json | 12 ++ .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 11 ++ .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 79 ++++++++ Tests/MbedTlsIntegration/CMakeLists.txt | 8 + Tests/MbedTlsIntegration/MbedTlsTestCert.c | 118 ++++++++++++ Tests/MbedTlsIntegration/MbedTlsTestCert.h | 40 +++++ Tests/MbedTlsIntegration/MbedTlsTestServer.c | 146 +++++++++++++++ Tests/MbedTlsIntegration/MbedTlsTestServer.h | 43 +++++ Tests/MbedTlsIntegration/SocketStream.c | 92 ++++++++++ Tests/MbedTlsIntegration/SocketStream.h | 20 +++ ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 169 +++++++++++++++++- Tests/Support/MbedTlsFake.c | 150 ++++++++++++++++ Tests/Support/MbedTlsFake.h | 25 +++ 13 files changed, 908 insertions(+), 5 deletions(-) create mode 100644 .vscode/c_cpp_properties.json create mode 100644 Tests/MbedTlsIntegration/MbedTlsTestCert.c create mode 100644 Tests/MbedTlsIntegration/MbedTlsTestCert.h create mode 100644 Tests/MbedTlsIntegration/MbedTlsTestServer.c create mode 100644 Tests/MbedTlsIntegration/MbedTlsTestServer.h create mode 100644 Tests/MbedTlsIntegration/SocketStream.c create mode 100644 Tests/MbedTlsIntegration/SocketStream.h diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000..5b67ff11 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,12 @@ +{ + "version": 4, + "configurations": [ + { + "name": "Linux (devcontainer, debug preset)", + "compileCommands": "${workspaceFolder}/build/debug/compile_commands.json", + "intelliSenseMode": "linux-gcc-x64", + "cStandard": "gnu11", + "cppStandard": "gnu++17" + } + ] +} diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 0a56e74c..76d47e64 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -1,5 +1,6 @@ #include "SolidSyslogMbedTlsStream.h" +#include #include #include #include @@ -59,10 +60,20 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru ) == 0; } if (ok) + { + mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); + mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); + } + if (ok) { mbedtls_ssl_init(&self->SslContext); ok = mbedtls_ssl_setup(&self->SslContext, &self->SslConfig) == 0; } + if (ok && (self->Config.ServerName != NULL)) + { + ok = mbedtls_ssl_set_hostname(&self->SslContext, self->Config.ServerName) == 0; + } if (ok) { mbedtls_ssl_set_bio(&self->SslContext, self, MbedTlsStream_BioSend, MbedTlsStream_BioRecv, NULL); diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 917607c6..2f21ae00 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -2,6 +2,7 @@ extern "C" { +#include #include #include "MbedTlsFake.h" @@ -41,6 +42,14 @@ TEST_GROUP(SolidSyslogMbedTlsStream) SolidSyslogMbedTlsStream_Destroy(handle); StreamFake_Destroy(transport); } + + /* Tests needing config tweaks (CaChain, Rng, ServerName, …) call this + * to release setup()'s pool slot, mutate `config`, then re-Create. */ + void ReCreateHandleWithUpdatedConfig() + { + SolidSyslogMbedTlsStream_Destroy(handle); + handle = SolidSyslogMbedTlsStream_Create(&config); + } }; // clang-format on @@ -283,3 +292,73 @@ TEST(SolidSyslogMbedTlsStream, BioRecvCallbackForwardsBufferToTransport) POINTERS_EQUAL(buffer, StreamFake_LastReadBuf(transport)); LONGS_EQUAL(sizeof(buffer), StreamFake_LastReadSize(transport)); } + +TEST(SolidSyslogMbedTlsStream, OpenSetsAuthmodeRequired) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfAuthmodeCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfAuthmodeConfigArg()); + LONGS_EQUAL(MBEDTLS_SSL_VERIFY_REQUIRED, MbedTlsFake_LastSslConfAuthmodeArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenWiresCaChainFromConfigAndNullCrl) +{ + /* Use a non-null marker pointer; the fake captures it without dereferencing. */ + static mbedtls_x509_crt caChainMarker; + config.CaChain = &caChainMarker; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfCaChainCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfCaChainConfigArg()); + POINTERS_EQUAL(&caChainMarker, MbedTlsFake_LastSslConfCaChainArg()); + POINTERS_EQUAL(nullptr, MbedTlsFake_LastSslConfCaChainCrlArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenWiresRngFromConfigUsingCtrDrbgRandom) +{ + static mbedtls_ctr_drbg_context rngMarker; + config.Rng = &rngMarker; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfRngCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfRngConfigArg()); + POINTERS_EQUAL((void*) mbedtls_ctr_drbg_random, (void*) MbedTlsFake_LastSslConfRngFuncArg()); + POINTERS_EQUAL(&rngMarker, MbedTlsFake_LastSslConfRngContextArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenSetsHostnameWhenServerNameProvided) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslSetHostnameCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslSetHostnameContextArg()); + STRCMP_EQUAL("syslog.example.com", MbedTlsFake_LastSslSetHostnameNameArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenSkipsHostnameWhenServerNameIsNull) +{ + /* setup() left config.ServerName at NULL. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(0, MbedTlsFake_SslSetHostnameCallCount()); +} diff --git a/Tests/MbedTlsIntegration/CMakeLists.txt b/Tests/MbedTlsIntegration/CMakeLists.txt index f7cb0151..8e94121e 100644 --- a/Tests/MbedTlsIntegration/CMakeLists.txt +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -47,6 +47,11 @@ endforeach() add_executable(MbedTlsIntegrationTests main.cpp SolidSyslogMbedTlsStreamIntegrationTest.cpp + SocketStream.c + MbedTlsTestCert.c + MbedTlsTestServer.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c ) target_link_libraries(MbedTlsIntegrationTests PRIVATE @@ -56,9 +61,12 @@ target_link_libraries(MbedTlsIntegrationTests PRIVATE mbedtls mbedx509 mbedcrypto + Threads::Threads ) target_include_directories(MbedTlsIntegrationTests PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface ${CMAKE_SOURCE_DIR}/Core/Source ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.c b/Tests/MbedTlsIntegration/MbedTlsTestCert.c new file mode 100644 index 00000000..79e9d96e --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.c @@ -0,0 +1,118 @@ +#include "MbedTlsTestCert.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum +{ + RSA_KEY_BITS = 2048, + RSA_EXPONENT = 65537, + DER_BUFFER_BYTES = 4096 +}; + +static void GenerateKey(mbedtls_pk_context* key, mbedtls_ctr_drbg_context* rng); +static void WriteCertToDer( + const struct MbedTlsTestCertConfig* config, + mbedtls_pk_context* subjectKey, + unsigned char* derBuffer, + size_t bufferSize, + size_t* derStartOffset, + size_t* derLength, + mbedtls_ctr_drbg_context* rng +); + +void MbedTlsTestCert_Create( + const struct MbedTlsTestCertConfig* config, + struct MbedTlsTestCert* out, + mbedtls_ctr_drbg_context* rng +) +{ + mbedtls_pk_init(&out->Key); + mbedtls_x509_crt_init(&out->Cert); + strncpy(out->SubjectName, config->SubjectName, sizeof(out->SubjectName) - 1U); + out->SubjectName[sizeof(out->SubjectName) - 1U] = '\0'; + + GenerateKey(&out->Key, rng); + + unsigned char derBuffer[DER_BUFFER_BYTES]; + size_t derStartOffset = 0U; + size_t derLength = 0U; + WriteCertToDer(config, &out->Key, derBuffer, sizeof(derBuffer), &derStartOffset, &derLength, rng); + + mbedtls_x509_crt_parse_der(&out->Cert, &derBuffer[derStartOffset], derLength); +} + +void MbedTlsTestCert_Destroy(struct MbedTlsTestCert* cert) +{ + mbedtls_x509_crt_free(&cert->Cert); + mbedtls_pk_free(&cert->Key); +} + +static void GenerateKey(mbedtls_pk_context* key, mbedtls_ctr_drbg_context* rng) +{ + mbedtls_pk_setup(key, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)); + mbedtls_rsa_gen_key(mbedtls_pk_rsa(*key), mbedtls_ctr_drbg_random, rng, RSA_KEY_BITS, RSA_EXPONENT); +} + +static void WriteCertToDer( + const struct MbedTlsTestCertConfig* config, + mbedtls_pk_context* subjectKey, + unsigned char* derBuffer, + size_t bufferSize, + size_t* derStartOffset, + size_t* derLength, + mbedtls_ctr_drbg_context* rng +) +{ + mbedtls_x509write_cert crt; + mbedtls_x509write_crt_init(&crt); + mbedtls_x509write_crt_set_md_alg(&crt, MBEDTLS_MD_SHA256); + mbedtls_x509write_crt_set_version(&crt, MBEDTLS_X509_CRT_VERSION_3); + mbedtls_x509write_crt_set_subject_key(&crt, subjectKey); + mbedtls_x509write_crt_set_subject_name(&crt, config->SubjectName); + + /* Self-signed when Issuer == NULL — issuer name + key match the subject. */ + const char* issuerName = (config->Issuer != NULL) ? config->Issuer->SubjectName : config->SubjectName; + mbedtls_pk_context* issuerKey = (config->Issuer != NULL) ? (mbedtls_pk_context*) &config->Issuer->Key : subjectKey; + mbedtls_x509write_crt_set_issuer_name(&crt, issuerName); + mbedtls_x509write_crt_set_issuer_key(&crt, issuerKey); + + const unsigned char serial[] = {0x01}; + mbedtls_x509write_crt_set_serial_raw(&crt, (unsigned char*) serial, sizeof(serial)); + + /* Long-validity test certs: 2024-01-01 to 2099-01-01. */ + mbedtls_x509write_crt_set_validity(&crt, "20240101000000", "20990101000000"); + + mbedtls_x509write_crt_set_basic_constraints(&crt, config->IsCa, -1); + + mbedtls_x509_san_list san; + if (config->SubjectAltDns != NULL) + { + memset(&san, 0, sizeof(san)); + san.node.type = MBEDTLS_X509_SAN_DNS_NAME; + san.node.san.unstructured_name.tag = MBEDTLS_ASN1_IA5_STRING; + san.node.san.unstructured_name.p = (unsigned char*) config->SubjectAltDns; + san.node.san.unstructured_name.len = strlen(config->SubjectAltDns); + san.next = NULL; + mbedtls_x509write_crt_set_subject_alternative_name(&crt, &san); + } + + /* mbedtls_x509write_crt_der writes at the END of the buffer, returning + * the number of bytes written. The actual DER starts at offset + * (bufferSize - written). */ + int written = mbedtls_x509write_crt_der(&crt, derBuffer, bufferSize, mbedtls_ctr_drbg_random, rng); + if (written > 0) + { + *derStartOffset = bufferSize - (size_t) written; + *derLength = (size_t) written; + } + + mbedtls_x509write_crt_free(&crt); +} diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.h b/Tests/MbedTlsIntegration/MbedTlsTestCert.h new file mode 100644 index 00000000..4816af55 --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.h @@ -0,0 +1,40 @@ +#ifndef MBEDTLSTESTCERT_H +#define MBEDTLSTESTCERT_H + +#include +#include +#include + +#include "ExternC.h" + +EXTERN_C_BEGIN + + struct MbedTlsTestCert + { + mbedtls_pk_context Key; + mbedtls_x509_crt Cert; + char SubjectName[128]; + }; + + struct MbedTlsTestCertConfig + { + const char* SubjectName; /* X.509 subject string, e.g. "CN=test-ca" */ + const char* SubjectAltDns; /* SAN dnsName; NULL = no SAN */ + int IsCa; /* 1 = mark BasicConstraints CA:TRUE */ + const struct MbedTlsTestCert* Issuer; /* NULL = self-signed */ + }; + + /* Build a fresh RSA-2048 key + cert pair. The cert is parsed back into + * `out->Cert` and is immediately usable by mbedtls_ssl_conf_ca_chain + * (CA) or mbedtls_ssl_conf_own_cert (server / mTLS client). */ + void MbedTlsTestCert_Create( + const struct MbedTlsTestCertConfig* config, + struct MbedTlsTestCert* out, + mbedtls_ctr_drbg_context* rng + ); + + void MbedTlsTestCert_Destroy(struct MbedTlsTestCert * cert); + +EXTERN_C_END + +#endif /* MBEDTLSTESTCERT_H */ diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.c b/Tests/MbedTlsIntegration/MbedTlsTestServer.c new file mode 100644 index 00000000..4a14f10d --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.c @@ -0,0 +1,146 @@ +#include "MbedTlsTestServer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MbedTlsTestCert.h" + +enum +{ + RECEIVE_BUFFER_BYTES = 1024 +}; + +struct MbedTlsTestServer +{ + int Fd; + mbedtls_ssl_config SslConfig; + mbedtls_ssl_context SslContext; + pthread_t Thread; + bool ThreadJoined; + bool HandshakeSucceeded; + unsigned char Received[RECEIVE_BUFFER_BYTES]; + size_t ReceivedLength; +}; + +static void* RunServer(void* arg); +static int ServerBioSend(void* ctx, const unsigned char* buf, size_t len); +static int ServerBioRecv(void* ctx, unsigned char* buf, size_t len); + +struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServerConfig* config) +{ + struct MbedTlsTestServer* self = (struct MbedTlsTestServer*) calloc(1U, sizeof(struct MbedTlsTestServer)); + self->Fd = config->ServerFd; + + mbedtls_ssl_config_init(&self->SslConfig); + mbedtls_ssl_config_defaults( + &self->SslConfig, + MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ); + mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, config->Rng); + /* Server-auth-only: don't require a client cert in slice 3 (mTLS lands + * in slice 5). */ + mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_own_cert( + &self->SslConfig, + (mbedtls_x509_crt*) &config->ServerCert->Cert, + (mbedtls_pk_context*) &config->ServerCert->Key + ); + + mbedtls_ssl_init(&self->SslContext); + mbedtls_ssl_setup(&self->SslContext, &self->SslConfig); + mbedtls_ssl_set_bio(&self->SslContext, &self->Fd, ServerBioSend, ServerBioRecv, NULL); + + pthread_create(&self->Thread, NULL, RunServer, self); + return self; +} + +void MbedTlsTestServer_Destroy(struct MbedTlsTestServer* self) +{ + if (self == NULL) + { + return; + } + if (!self->ThreadJoined) + { + /* Worker might still be blocked in recv. Shutting the fd unblocks + * it; close gets called below after the join. */ + if (self->Fd >= 0) + { + shutdown(self->Fd, SHUT_RDWR); + } + pthread_join(self->Thread, NULL); + self->ThreadJoined = true; + } + mbedtls_ssl_free(&self->SslContext); + mbedtls_ssl_config_free(&self->SslConfig); + if (self->Fd >= 0) + { + close(self->Fd); + } + free(self); +} + +bool MbedTlsTestServer_JoinAndHandshakeSucceeded(struct MbedTlsTestServer* self) +{ + /* RunServer exits naturally once the client closes its end (recv returns + * 0 → handshake or read sees EOF). Tests should close the client side + * before calling Join so this returns promptly. */ + if (!self->ThreadJoined) + { + pthread_join(self->Thread, NULL); + self->ThreadJoined = true; + } + return self->HandshakeSucceeded; +} + +const unsigned char* MbedTlsTestServer_ReceivedBytes(struct MbedTlsTestServer* self) +{ + return self->Received; +} + +size_t MbedTlsTestServer_ReceivedLength(struct MbedTlsTestServer* self) +{ + return self->ReceivedLength; +} + +static void* RunServer(void* arg) +{ + struct MbedTlsTestServer* self = (struct MbedTlsTestServer*) arg; + + int handshakeRc = 0; + do + { + handshakeRc = mbedtls_ssl_handshake(&self->SslContext); + } while ((handshakeRc == MBEDTLS_ERR_SSL_WANT_READ) || (handshakeRc == MBEDTLS_ERR_SSL_WANT_WRITE)); + + self->HandshakeSucceeded = (handshakeRc == 0); + + /* Slice 3 only pins handshake outcome; the thread exits as soon as the + * handshake settles. Reading TLS bytes from the server side (and any + * blocking that implies) is left for a future slice when a test actually + * needs it. */ + return NULL; +} + +static int ServerBioSend(void* ctx, const unsigned char* buf, size_t len) +{ + int fd = *(int*) ctx; + ssize_t n = send(fd, buf, len, 0); + return (n >= 0) ? (int) n : -1; +} + +static int ServerBioRecv(void* ctx, unsigned char* buf, size_t len) +{ + int fd = *(int*) ctx; + ssize_t n = recv(fd, buf, len, 0); + return (n >= 0) ? (int) n : -1; +} diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.h b/Tests/MbedTlsIntegration/MbedTlsTestServer.h new file mode 100644 index 00000000..bc9108dc --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.h @@ -0,0 +1,43 @@ +#ifndef MBEDTLSTESTSERVER_H +#define MBEDTLSTESTSERVER_H + +#include +#include +#include + +#include "ExternC.h" + +struct MbedTlsTestCert; + +EXTERN_C_BEGIN + + struct MbedTlsTestServer; + + struct MbedTlsTestServerConfig + { + int ServerFd; /* one end of a socketpair; ownership transferred to the server */ + const struct MbedTlsTestCert* ServerCert; /* server's cert + matching key */ + mbedtls_ctr_drbg_context* Rng; /* shared with the test fixture */ + }; + + /* Spawns a worker thread that drives the server-side handshake and then + * reads bytes until the peer closes. The Server owns ServerFd from this + * point on. */ + struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServerConfig* config); + + /* Joins the worker thread and tears down all mbedTLS state. */ + void MbedTlsTestServer_Destroy(struct MbedTlsTestServer * self); + + /* Wait for the worker thread to exit and return whether the handshake + * completed successfully. Callable once per server. */ + bool MbedTlsTestServer_JoinAndHandshakeSucceeded(struct MbedTlsTestServer * self); + + /* After Join: the bytes the server read via mbedtls_ssl_read after a + * successful handshake (zero-length if the handshake failed or the peer + * closed without sending). */ + const unsigned char* MbedTlsTestServer_ReceivedBytes(struct MbedTlsTestServer * self); + size_t MbedTlsTestServer_ReceivedLength(struct MbedTlsTestServer * self); + +EXTERN_C_END + +#endif /* MBEDTLSTESTSERVER_H */ diff --git a/Tests/MbedTlsIntegration/SocketStream.c b/Tests/MbedTlsIntegration/SocketStream.c new file mode 100644 index 00000000..86fb1739 --- /dev/null +++ b/Tests/MbedTlsIntegration/SocketStream.c @@ -0,0 +1,92 @@ +#include "SocketStream.h" + +#include +#include +#include +#include + +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" + +struct SolidSyslogAddress; + +struct SocketStream +{ + struct SolidSyslogStream Base; + int Fd; +}; + +static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void Close(struct SolidSyslogStream* self); + +struct SolidSyslogStream* SocketStream_Create(int fd) +{ + struct SocketStream* stream = (struct SocketStream*) calloc(1, sizeof(struct SocketStream)); + stream->Base.Open = Open; + stream->Base.Send = Send; + stream->Base.Read = Read; + stream->Base.Close = Close; + stream->Fd = fd; + return &stream->Base; +} + +void SocketStream_Destroy(struct SolidSyslogStream* self) +{ + struct SocketStream* stream = (struct SocketStream*) self; + /* Idempotent close: if the production Close path already ran it will + * have set Fd to -1. Otherwise (e.g. handshake failure where Close + * never runs) we still own the fd and must release it so the server's + * blocking recv unblocks promptly. */ + if (stream->Fd >= 0) + { + close(stream->Fd); + stream->Fd = -1; + } + free(self); +} + +static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +{ + /* The fd is pre-connected (e.g. socketpair); Open is a no-op so the test + * harness fully controls connection lifecycle. */ + (void) self; + (void) addr; + return true; +} + +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +{ + struct SocketStream* stream = (struct SocketStream*) self; + const unsigned char* bytes = (const unsigned char*) buffer; + size_t remaining = size; + while (remaining > 0) + { + ssize_t n = send(stream->Fd, bytes, remaining, 0); + if (n <= 0) + { + return false; + } + bytes += n; + remaining -= (size_t) n; + } + return true; +} + +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) +{ + struct SocketStream* stream = (struct SocketStream*) self; + ssize_t n = recv(stream->Fd, buffer, size, 0); + return (SolidSyslogSsize) n; +} + +static void Close(struct SolidSyslogStream* self) +{ + struct SocketStream* stream = (struct SocketStream*) self; + if (stream->Fd >= 0) + { + close(stream->Fd); + stream->Fd = -1; + } +} diff --git a/Tests/MbedTlsIntegration/SocketStream.h b/Tests/MbedTlsIntegration/SocketStream.h new file mode 100644 index 00000000..c64cc949 --- /dev/null +++ b/Tests/MbedTlsIntegration/SocketStream.h @@ -0,0 +1,20 @@ +#ifndef SOCKETSTREAM_H +#define SOCKETSTREAM_H + +#include "ExternC.h" + +struct SolidSyslogStream; + +EXTERN_C_BEGIN + + /* Minimal SolidSyslogStream wrapper over a raw socket fd. Open is a + * no-op (the fd is supplied at Create time and assumed already-connected, + * e.g. one end of a Unix socketpair). Send / Read use blocking write() + * and read(); Close closes the fd. Used by the integration tests as the + * client-side transport injected into SolidSyslogMbedTlsStream. */ + struct SolidSyslogStream* SocketStream_Create(int fd); + void SocketStream_Destroy(struct SolidSyslogStream * self); + +EXTERN_C_END + +#endif /* SOCKETSTREAM_H */ diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index c1395a8a..fad1a2d1 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -2,15 +2,174 @@ extern "C" { +#include +#include +#include #include +#include +#include + +#include "MbedTlsTestCert.h" +#include "MbedTlsTestServer.h" +#include "SocketStream.h" +#include "SolidSyslogAddress.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStream.h" +} + +namespace +{ +constexpr const char* kServerHostname = "syslog.example.com"; +constexpr const char* kCaSubject = "CN=Test Root CA"; +constexpr const char* kServerSubject = "CN=syslog.example.com"; + +void NoOpSleep(int milliseconds) +{ + (void) milliseconds; +} +} // namespace + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) +{ + mbedtls_entropy_context entropy = {}; + mbedtls_ctr_drbg_context rng = {}; + int fds[2] = {-1, -1}; + struct MbedTlsTestCert trustedCa = {}; + struct MbedTlsTestCert serverCert = {}; + struct MbedTlsTestServer* server = nullptr; + struct SolidSyslogStream* clientTransport = nullptr; + struct SolidSyslogStream* tlsStream = nullptr; + + void setup() override + { + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&rng); + const unsigned char pers[] = "mbedtls-integration-test"; + mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &entropy, pers, sizeof(pers) - 1U); + + socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + + struct MbedTlsTestCertConfig caConfig = {}; + caConfig.SubjectName = kCaSubject; + caConfig.IsCa = 1; + MbedTlsTestCert_Create(&caConfig, &trustedCa, &rng); + + struct MbedTlsTestCertConfig serverConfig = {}; + serverConfig.SubjectName = kServerSubject; + serverConfig.SubjectAltDns = kServerHostname; + serverConfig.IsCa = 0; + serverConfig.Issuer = &trustedCa; + MbedTlsTestCert_Create(&serverConfig, &serverCert, &rng); + } + + void teardown() override + { + if (tlsStream != nullptr) + { + SolidSyslogMbedTlsStream_Destroy(tlsStream); + } + if (clientTransport != nullptr) + { + SocketStream_Destroy(clientTransport); + } + if (server != nullptr) + { + MbedTlsTestServer_Destroy(server); + } + MbedTlsTestCert_Destroy(&serverCert); + MbedTlsTestCert_Destroy(&trustedCa); + mbedtls_ctr_drbg_free(&rng); + mbedtls_entropy_free(&entropy); + } + + struct SolidSyslogStream* StartServerWithCert(const struct MbedTlsTestCert* cert) + { + struct MbedTlsTestServerConfig serverConfig = {}; + serverConfig.ServerFd = fds[1]; + serverConfig.ServerCert = cert; + serverConfig.Rng = &rng; + server = MbedTlsTestServer_Create(&serverConfig); + clientTransport = SocketStream_Create(fds[0]); + return clientTransport; + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeSucceedsWhenServerCertSignedByTrustedCaAndHostnameMatches) +{ + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &trustedCa.Cert; + config.ServerName = kServerHostname; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_TRUE_TEXT(opened, "client-side Open (incl. handshake) should succeed against a trusted server"); + CHECK_TRUE_TEXT( + MbedTlsTestServer_JoinAndHandshakeSucceeded(server), + "server-side handshake should mirror the client's success" + ); } -// Slice 1 (plumbing) skeleton: proves the integration-test binary links -// against real libmbedtls built from $MBEDTLS_DIR. The handshake-driving -// in-process TLS server lands in slice 3 alongside the certificate -// validation tests. +/* TODO(slice-3 follow-up): negative-path handshake currently deadlocks — + * the client's mbedtls_ssl_handshake returns the verify error, but the server + * thread is still blocked in the handshake (likely a TLS 1.3 vs 1.2 sequencing + * issue mirroring the OpenSSL TlsTestServer "pin TLS 1.2 for negative tests" + * comment). Re-enable after pinning the server's max version to TLS 1.2 or + * after introducing an mbedtls_ssl_conf_max_tls_version-equivalent on the + * server. The happy-path test above proves the full handshake works + * end-to-end against real libmbedtls. */ +IGNORE_TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUntrustedCa) +{ + /* Trust an unrelated CA: we hand the *client* a different CA chain than + * the one that signed the server's cert, so the chain validation fails. */ + struct MbedTlsTestCert untrustedCa = {}; + struct MbedTlsTestCertConfig untrustedConfig = {}; + untrustedConfig.SubjectName = "CN=Wrong Root CA"; + untrustedConfig.IsCa = 1; + MbedTlsTestCert_Create(&untrustedConfig, &untrustedCa, &rng); + + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &untrustedCa.Cert; + config.ServerName = kServerHostname; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); -TEST_GROUP(SolidSyslogMbedTlsStreamIntegration){}; + CHECK_FALSE_TEXT(opened, "client-side handshake must fail when the server cert chains to an untrusted CA"); + + MbedTlsTestCert_Destroy(&untrustedCa); +} + +/* TODO(slice-3 follow-up): see comment above HandshakeFailsWhenServerCertSignedByUntrustedCa. */ +IGNORE_TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMatchCert) +{ + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &trustedCa.Cert; + config.ServerName = "wrong-host.example.com"; /* server cert has SAN syslog.example.com */ + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_FALSE_TEXT(opened, "client-side handshake must fail when ServerName does not match the cert's SAN"); +} TEST(SolidSyslogMbedTlsStreamIntegration, BinaryLinksAgainstRealLibMbedTls) { diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index ba25d78c..f8c96802 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -70,6 +70,29 @@ static mbedtls_ssl_context* lastSslFreeArg; static int sslConfigFreeCallCount; static mbedtls_ssl_config* lastSslConfigFreeArg; +/* mbedtls_ssl_conf_authmode */ +static int sslConfAuthmodeCallCount; +static mbedtls_ssl_config* lastSslConfAuthmodeConfigArg; +static int lastSslConfAuthmodeArg; + +/* mbedtls_ssl_conf_ca_chain */ +static int sslConfCaChainCallCount; +static mbedtls_ssl_config* lastSslConfCaChainConfigArg; +static mbedtls_x509_crt* lastSslConfCaChainArg; +static mbedtls_x509_crl* lastSslConfCaChainCrlArg; + +/* mbedtls_ssl_conf_rng */ +typedef int (*RngFunc)(void*, unsigned char*, size_t); +static int sslConfRngCallCount; +static mbedtls_ssl_config* lastSslConfRngConfigArg; +static RngFunc lastSslConfRngFuncArg; +static void* lastSslConfRngContextArg; + +/* mbedtls_ssl_set_hostname */ +static int sslSetHostnameCallCount; +static mbedtls_ssl_context* lastSslSetHostnameContextArg; +static const char* lastSslSetHostnameNameArg; + /* ------------------------------------------------------------------------- * Test accessors. * ------------------------------------------------------------------------- */ @@ -114,6 +137,20 @@ void MbedTlsFake_Reset(void) lastSslFreeArg = NULL; sslConfigFreeCallCount = 0; lastSslConfigFreeArg = NULL; + sslConfAuthmodeCallCount = 0; + lastSslConfAuthmodeConfigArg = NULL; + lastSslConfAuthmodeArg = 0; + sslConfCaChainCallCount = 0; + lastSslConfCaChainConfigArg = NULL; + lastSslConfCaChainArg = NULL; + lastSslConfCaChainCrlArg = NULL; + sslConfRngCallCount = 0; + lastSslConfRngConfigArg = NULL; + lastSslConfRngFuncArg = NULL; + lastSslConfRngContextArg = NULL; + sslSetHostnameCallCount = 0; + lastSslSetHostnameContextArg = NULL; + lastSslSetHostnameNameArg = NULL; } int MbedTlsFake_SslConfigInitCallCount(void) @@ -297,6 +334,76 @@ mbedtls_ssl_config* MbedTlsFake_LastSslConfigFreeArg(void) return lastSslConfigFreeArg; } +int MbedTlsFake_SslConfAuthmodeCallCount(void) +{ + return sslConfAuthmodeCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfAuthmodeConfigArg(void) +{ + return lastSslConfAuthmodeConfigArg; +} + +int MbedTlsFake_LastSslConfAuthmodeArg(void) +{ + return lastSslConfAuthmodeArg; +} + +int MbedTlsFake_SslConfCaChainCallCount(void) +{ + return sslConfCaChainCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfCaChainConfigArg(void) +{ + return lastSslConfCaChainConfigArg; +} + +mbedtls_x509_crt* MbedTlsFake_LastSslConfCaChainArg(void) +{ + return lastSslConfCaChainArg; +} + +mbedtls_x509_crl* MbedTlsFake_LastSslConfCaChainCrlArg(void) +{ + return lastSslConfCaChainCrlArg; +} + +int MbedTlsFake_SslConfRngCallCount(void) +{ + return sslConfRngCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfRngConfigArg(void) +{ + return lastSslConfRngConfigArg; +} + +RngFunc MbedTlsFake_LastSslConfRngFuncArg(void) +{ + return lastSslConfRngFuncArg; +} + +void* MbedTlsFake_LastSslConfRngContextArg(void) +{ + return lastSslConfRngContextArg; +} + +int MbedTlsFake_SslSetHostnameCallCount(void) +{ + return sslSetHostnameCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslSetHostnameContextArg(void) +{ + return lastSslSetHostnameContextArg; +} + +const char* MbedTlsFake_LastSslSetHostnameNameArg(void) +{ + return lastSslSetHostnameNameArg; +} + /* ------------------------------------------------------------------------- * Link-interposed mbedTLS symbols. The test executable does not link * libmbedtls; the production code's calls to mbedtls_* resolve here. @@ -393,3 +500,46 @@ void mbedtls_ssl_config_free(mbedtls_ssl_config* conf) sslConfigFreeCallCount++; lastSslConfigFreeArg = conf; } + +void mbedtls_ssl_conf_authmode(mbedtls_ssl_config* conf, int authmode) +{ + sslConfAuthmodeCallCount++; + lastSslConfAuthmodeConfigArg = conf; + lastSslConfAuthmodeArg = authmode; +} + +void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config* conf, mbedtls_x509_crt* ca_chain, mbedtls_x509_crl* ca_crl) +{ + sslConfCaChainCallCount++; + lastSslConfCaChainConfigArg = conf; + lastSslConfCaChainArg = ca_chain; + lastSslConfCaChainCrlArg = ca_crl; +} + +void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, RngFunc f_rng, void* p_rng) +{ + sslConfRngCallCount++; + lastSslConfRngConfigArg = conf; + lastSslConfRngFuncArg = f_rng; + lastSslConfRngContextArg = p_rng; +} + +/* Stub: the production code takes the address of mbedtls_ctr_drbg_random when + * wiring conf_rng. The fake never actually invokes the function via the + * captured pointer, so this body never runs at test time. Defined here so the + * symbol resolves at link time without pulling in real libmbedcrypto. */ +int mbedtls_ctr_drbg_random(void* p_rng, unsigned char* output, size_t output_len) +{ + (void) p_rng; + (void) output; + (void) output_len; + return 0; +} + +int mbedtls_ssl_set_hostname(mbedtls_ssl_context* ssl, const char* hostname) +{ + sslSetHostnameCallCount++; + lastSslSetHostnameContextArg = ssl; + lastSslSetHostnameNameArg = hostname; + return 0; +} diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index 35ace4ea..78d3339e 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -7,6 +7,9 @@ struct mbedtls_ssl_config; struct mbedtls_ssl_context; +struct mbedtls_x509_crt; +struct mbedtls_x509_crl; +struct mbedtls_ctr_drbg_context; EXTERN_C_BEGIN @@ -72,6 +75,28 @@ EXTERN_C_BEGIN int MbedTlsFake_SslConfigFreeCallCount(void); struct mbedtls_ssl_config* MbedTlsFake_LastSslConfigFreeArg(void); + /* mbedtls_ssl_conf_authmode */ + int MbedTlsFake_SslConfAuthmodeCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfAuthmodeConfigArg(void); + int MbedTlsFake_LastSslConfAuthmodeArg(void); + + /* mbedtls_ssl_conf_ca_chain */ + int MbedTlsFake_SslConfCaChainCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfCaChainConfigArg(void); + struct mbedtls_x509_crt* MbedTlsFake_LastSslConfCaChainArg(void); + struct mbedtls_x509_crl* MbedTlsFake_LastSslConfCaChainCrlArg(void); + + /* mbedtls_ssl_conf_rng */ + int MbedTlsFake_SslConfRngCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfRngConfigArg(void); + int (*MbedTlsFake_LastSslConfRngFuncArg(void))(void*, unsigned char*, size_t); + void* MbedTlsFake_LastSslConfRngContextArg(void); + + /* mbedtls_ssl_set_hostname */ + int MbedTlsFake_SslSetHostnameCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslSetHostnameContextArg(void); + const char* MbedTlsFake_LastSslSetHostnameNameArg(void); + EXTERN_C_END #endif /* MBEDTLSFAKE_H */ From 1372cc963fe12010cf6352806634f25bcde71d84 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 09:41:47 +0000 Subject: [PATCH 04/13] fix: S08.07 slice 3 unblock negative-handshake integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two negative-path tests (untrusted-CA, hostname-mismatch) landed as IGNORE in the slice-3 commit because they deadlocked the harness. Three fixes together turn them green: - Pin the in-process test server to TLS 1.2 so cert-rejection is synchronous within the server's own handshake (TLS 1.3 sends the cert in a single flight then blocks on ClientFinished, leaving the pthread worker stuck during teardown). Mirrors the OpenSSL TlsTestServer pin. - Ignore SIGPIPE in the test main so a send to a peer fd the other side has already closed during teardown surfaces as EPIPE instead of aborting the process. The BIO callbacks don't pass MSG_NOSIGNAL. - Track IsOpen on MbedTlsStream and call Close from Cleanup, mirroring the OpenSSL TlsStream pattern. Without this, destroying an Open stream leaked the mbedTLS SSL context/config — only exposed once the negative paths actually ran under ASan. The pool test now wires a StreamFake transport (matching the OpenSSL pool-test setup) so the vtable-routed Close in Cleanup has somewhere to land. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 19 ++++++++++++++++--- .../Source/SolidSyslogMbedTlsStreamPrivate.h | 5 +++++ Tests/MbedTls/CMakeLists.txt | 2 ++ .../SolidSyslogMbedTlsStreamPoolTest.cpp | 14 +++++++++++++- Tests/MbedTlsIntegration/MbedTlsTestServer.c | 11 +++++++++++ ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 13 ++----------- Tests/MbedTlsIntegration/main.cpp | 9 +++++++++ 7 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 76d47e64..b33cf722 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -31,10 +31,15 @@ void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct Solid self->Base.Read = MbedTlsStream_Read; self->Base.Close = MbedTlsStream_Close; self->Config = *config; + self->IsOpen = false; } void MbedTlsStream_Cleanup(struct SolidSyslogStream* base) { + /* Mirror the OpenSSL TlsStream pattern: an integrator who destroys a + * still-Open stream must not leak the underlying TLS state. Close + * guards on IsOpen so Cleanup is safe even when Open was never reached. */ + MbedTlsStream_Close(base); /* Overwrite the abstract base with the shared NullStream vtable so * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ *base = *SolidSyslogNullStream_Get(); @@ -52,6 +57,10 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru if (ok) { mbedtls_ssl_config_init(&self->SslConfig); + /* Flip IsOpen now so any subsequent allocation failure in this + * function still leaves the struct in a Close-safe state — the + * partially-initialised config/context must reach mbedtls_*_free. */ + self->IsOpen = true; ok = mbedtls_ssl_config_defaults( &self->SslConfig, MBEDTLS_SSL_IS_CLIENT, @@ -126,8 +135,12 @@ static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base static inline void MbedTlsStream_Close(struct SolidSyslogStream* base) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); - (void) mbedtls_ssl_close_notify(&self->SslContext); - mbedtls_ssl_free(&self->SslContext); - mbedtls_ssl_config_free(&self->SslConfig); + if (self->IsOpen) + { + (void) mbedtls_ssl_close_notify(&self->SslContext); + mbedtls_ssl_free(&self->SslContext); + mbedtls_ssl_config_free(&self->SslConfig); + self->IsOpen = false; + } SolidSyslogStream_Close(self->Config.Transport); } diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h index e6661a89..27a28e00 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -2,6 +2,7 @@ #define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H #include +#include #include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogStreamDefinition.h" @@ -12,6 +13,10 @@ struct SolidSyslogMbedTlsStream struct SolidSyslogMbedTlsStreamConfig Config; mbedtls_ssl_config SslConfig; mbedtls_ssl_context SslContext; + /* Tracks whether Open has reached the point of mbedTLS state allocation. + * Close guards its mbedtls_*_free calls on this so an unopened stream + * can be destroyed without touching uninitialised structs. */ + bool IsOpen; }; void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config); diff --git a/Tests/MbedTls/CMakeLists.txt b/Tests/MbedTls/CMakeLists.txt index b45e140c..2ace48d8 100644 --- a/Tests/MbedTls/CMakeLists.txt +++ b/Tests/MbedTls/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(SolidSyslogMbedTlsStreamPoolTest main.cpp ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c + ${CMAKE_SOURCE_DIR}/Tests/StreamFake.c ) target_link_libraries(SolidSyslogMbedTlsStreamPoolTest PRIVATE @@ -28,6 +29,7 @@ target_include_directories(SolidSyslogMbedTlsStreamPoolTest PRIVATE ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface ${CMAKE_SOURCE_DIR}/Core/Interface ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests ) # cppcheck cannot resolve mbedTLS's `#include MBEDTLS_USER_CONFIG_FILE` diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp index 82e8e818..72c7039c 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp @@ -11,6 +11,7 @@ extern "C" #include "SolidSyslogStream.h" #include "SolidSyslogStreamDefinition.h" #include "SolidSyslogTunables.h" +#include "StreamFake.h" } #include "TestUtils.h" @@ -36,11 +37,21 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-f // clang-format off TEST_GROUP(SolidSyslogMbedTlsStreamPool) { - struct SolidSyslogMbedTlsStreamConfig config = {}; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogMbedTlsStreamConfig config = {}; // cppcheck-suppress constVariable -- assigned in test bodies; cppcheck does not model CppUTest lifecycle struct SolidSyslogStream* pooled[SOLIDSYSLOG_MBED_TLS_STREAM_POOL_SIZE] = {}; struct SolidSyslogStream* overflow = nullptr; + void setup() override + { + /* A real transport handle keeps Destroy → Cleanup → Close on the + * vtable-routed transport safe; mirrors the OpenSSL TlsStream + * pool-test pattern. */ + transport = StreamFake_Create(); + config.Transport = transport; + } + void teardown() override { for (auto* handle : pooled) @@ -55,6 +66,7 @@ TEST_GROUP(SolidSyslogMbedTlsStreamPool) { SolidSyslogMbedTlsStream_Destroy(overflow); } + StreamFake_Destroy(transport); ConfigLockFake_Uninstall(); ErrorHandlerFake_Uninstall(); } diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.c b/Tests/MbedTlsIntegration/MbedTlsTestServer.c index 4a14f10d..b8ab4d82 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestServer.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.c @@ -45,6 +45,17 @@ struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServe MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT ); + /* Pin TLS 1.2 — mirrors the rationale in the OpenSSL TlsTestServer + * (Tests/OpenSslIntegration/TlsTestServer.c:37). In TLS 1.3 the server + * sends Certificate/CertVerify/Finished in one flight and then blocks + * in recv waiting for ClientFinished. On the negative paths the client's + * verify fails after that flight is read; even if the client sends an + * Alert, the server's blocking recv on the socketpair lands the test in + * a teardown-order deadlock. TLS 1.2's request/response cadence keeps + * the rejection synchronous: the server is waiting for ClientKeyExchange + * when the client closes its end, so server-side recv returns 0 and the + * worker exits cleanly. */ + mbedtls_ssl_conf_max_tls_version(&self->SslConfig, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, config->Rng); /* Server-auth-only: don't require a client cert in slice 3 (mTLS lands * in slice 5). */ diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index fad1a2d1..62230385 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -118,15 +118,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeSucceedsWhenServerCertSignedB ); } -/* TODO(slice-3 follow-up): negative-path handshake currently deadlocks — - * the client's mbedtls_ssl_handshake returns the verify error, but the server - * thread is still blocked in the handshake (likely a TLS 1.3 vs 1.2 sequencing - * issue mirroring the OpenSSL TlsTestServer "pin TLS 1.2 for negative tests" - * comment). Re-enable after pinning the server's max version to TLS 1.2 or - * after introducing an mbedtls_ssl_conf_max_tls_version-equivalent on the - * server. The happy-path test above proves the full handshake works - * end-to-end against real libmbedtls. */ -IGNORE_TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUntrustedCa) +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUntrustedCa) { /* Trust an unrelated CA: we hand the *client* a different CA chain than * the one that signed the server's cert, so the chain validation fails. */ @@ -153,8 +145,7 @@ IGNORE_TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSig MbedTlsTestCert_Destroy(&untrustedCa); } -/* TODO(slice-3 follow-up): see comment above HandshakeFailsWhenServerCertSignedByUntrustedCa. */ -IGNORE_TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMatchCert) +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMatchCert) { struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); struct SolidSyslogMbedTlsStreamConfig config = {}; diff --git a/Tests/MbedTlsIntegration/main.cpp b/Tests/MbedTlsIntegration/main.cpp index abc587a2..daba081f 100644 --- a/Tests/MbedTlsIntegration/main.cpp +++ b/Tests/MbedTlsIntegration/main.cpp @@ -1,6 +1,15 @@ +#include + #include "CppUTest/CommandLineTestRunner.h" int main(int argc, char* argv[]) { + /* Negative-path handshake tests close the client fd while the server + * thread may still be writing (e.g. its own close_notify in response to + * the client's alert). Neither our BIO callbacks nor SocketStream pass + * MSG_NOSIGNAL, so a write to a closed peer would raise SIGPIPE and + * abort the test process. Ignoring SIGPIPE turns the failure into an + * EPIPE that mbedTLS / the BIO surface as a normal transport error. */ + signal(SIGPIPE, SIG_IGN); return CommandLineTestRunner::RunAllTests(argc, argv); } From 75bf3a3c613e8234ed6560790ad8bf91acd07d3c Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 09:57:37 +0000 Subject: [PATCH 05/13] feat: S08.07 slice 4 MbedTlsStream bounded handshake retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mbedtls_ssl_handshake under non-blocking transport can return MBEDTLS_ERR_SSL_WANT_READ / WANT_WRITE between RTTs while the multi-leg handshake progresses. Open now drives it to completion within a 5s budget (1ms poll interval), sleeping via the injected SolidSyslogSleepFunction between retries — same shape and constants as the OpenSSL TlsStream_PerformHandshake loop. Non-WANT errors are fail-fast: no retry, no Sleep, Open returns false on the first call. Test surface: - MbedTlsFake gains SetSslHandshakeReturnSequence so unit tests can drive WANT_READ → 0, WANT_WRITE → 0, and persistent-WANT sequences. - NoOpSleep in the unit test is now instrumented (call count + last args) via TestUtils' CALLED_FUNCTION macro. - Five new tests: retry-on-WANT_READ, sleep-between-retries, retry-on-WANT_WRITE, fail-on-persistent-WANT (budget), fail-immediately-on-hard-error. The integration tests still use blocking socketpair BIOs so the loop isn't exercised end-to-end yet — that arrives in slice 5 (mTLS) or a later non-blocking-BIO scaffolding pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 57 ++++++++++- .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 94 ++++++++++++++++++- Tests/Support/MbedTlsFake.c | 27 +++++- Tests/Support/MbedTlsFake.h | 5 + 4 files changed, 180 insertions(+), 3 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index b33cf722..29b0886f 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -10,10 +10,23 @@ #include "SolidSyslogStream.h" #include "SolidSyslogStreamDefinition.h" +enum +{ + /* Bounded retry budget for the TLS handshake under non-blocking transport. + Mirrors the OpenSSL TlsStream constants (5s comfortably covers WAN + deployments without burning the service thread indefinitely on a + wedged peer). */ + HANDSHAKE_TIMEOUT_MILLISECONDS = 5000, + HANDSHAKE_POLL_INTERVAL_MILLISECONDS = 1 +}; + struct SolidSyslogAddress; static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base); static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); +static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStream* self); +static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc); +static inline bool MbedTlsStream_IsHandshakeBudgetExhausted(int totalSleptMs); static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size); static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size); static inline void MbedTlsStream_Close(struct SolidSyslogStream* base); @@ -86,11 +99,53 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru if (ok) { mbedtls_ssl_set_bio(&self->SslContext, self, MbedTlsStream_BioSend, MbedTlsStream_BioRecv, NULL); - ok = mbedtls_ssl_handshake(&self->SslContext) == 0; + ok = MbedTlsStream_PerformHandshake(self); } return ok; } +/* Drive mbedtls_ssl_handshake to completion under non-blocking transport. + * Each call may return WANT_READ/WANT_WRITE while waiting for the multi-RTT + * handshake to progress; we sleep briefly between attempts (avoiding a busy + * spin) until either the handshake completes, hits a hard error, or the + * bounded budget expires. Same shape as OpenSSL's TlsStream_PerformHandshake. */ +static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStream* self) +{ + int totalSleptMs = 0; + bool result = false; + bool done = false; + + while (!done) + { + int rc = mbedtls_ssl_handshake(&self->SslContext); + if (rc == 0) + { + result = true; + done = true; + } + else if (!MbedTlsStream_IsRetryableHandshakeRc(rc) || MbedTlsStream_IsHandshakeBudgetExhausted(totalSleptMs)) + { + done = true; + } + else + { + self->Config.Sleep(HANDSHAKE_POLL_INTERVAL_MILLISECONDS); + totalSleptMs += HANDSHAKE_POLL_INTERVAL_MILLISECONDS; + } + } + return result; +} + +static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc) +{ + return (rc == MBEDTLS_ERR_SSL_WANT_READ) || (rc == MBEDTLS_ERR_SSL_WANT_WRITE); +} + +static inline bool MbedTlsStream_IsHandshakeBudgetExhausted(int totalSleptMs) +{ + return totalSleptMs >= HANDSHAKE_TIMEOUT_MILLISECONDS; +} + static int MbedTlsStream_BioSend(void* ctx, const unsigned char* buf, size_t len) { struct SolidSyslogMbedTlsStream* self = (struct SolidSyslogMbedTlsStream*) ctx; diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 2f21ae00..7cd1b151 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -13,11 +13,19 @@ extern "C" #include "StreamFake.h" } +#include "TestUtils.h" + +using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER/TWICE into scope for CALLED_FUNCTION / CALLED_FAKE + namespace { +int NoOpSleepCallCount; +int g_lastSleepMs; + void NoOpSleep(int milliseconds) { - (void) milliseconds; + NoOpSleepCallCount++; + g_lastSleepMs = milliseconds; } } // namespace @@ -31,6 +39,8 @@ TEST_GROUP(SolidSyslogMbedTlsStream) void setup() override { MbedTlsFake_Reset(); + NoOpSleepCallCount = 0; + g_lastSleepMs = 0; transport = StreamFake_Create(); config.Transport = transport; config.Sleep = NoOpSleep; @@ -50,6 +60,23 @@ TEST_GROUP(SolidSyslogMbedTlsStream) SolidSyslogMbedTlsStream_Destroy(handle); handle = SolidSyslogMbedTlsStream_Create(&config); } + + /* Arrange mbedtls_ssl_handshake to first emit `wantError`, then succeed on + * the next call — exercises the bounded handshake retry loop's progress + * path. mbedTLS returns the error code directly (no get_error indirection). */ + static void ArrangeHandshakeRetryThenSucceed(int wantError) + { + int seq[] = {wantError, 0}; + MbedTlsFake_SetSslHandshakeReturnSequence(seq, 2); + } + + /* Arrange mbedtls_ssl_handshake to fail with `errorCode` on every call — + * used both for the persistent-WANT (budget-exhausted) and hard-error paths. */ + static void ArrangePersistentHandshakeError(int errorCode) + { + int seq[] = {errorCode}; + MbedTlsFake_SetSslHandshakeReturnSequence(seq, 1); + } }; // clang-format on @@ -154,6 +181,71 @@ TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails) CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); } +/* ------------------------------------------------------------------------- + * Bounded handshake retry loop. mbedtls_ssl_handshake under non-blocking + * transport will emit MBEDTLS_ERR_SSL_WANT_READ / WANT_WRITE between RTTs; + * the loop must drive it to completion within a bounded budget so a wedged + * peer doesn't burn the service thread indefinitely. Mirrors the OpenSSL + * TlsStream pattern (Tests/SolidSyslogTlsStreamTest.cpp). + * ------------------------------------------------------------------------- */ + +TEST(SolidSyslogMbedTlsStream, OpenRetriesHandshakeOnWantRead) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_READ); + + CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); + CALLED_FAKE(MbedTlsFake_SslHandshake, TWICE); +} + +TEST(SolidSyslogMbedTlsStream, OpenSleepsBetweenHandshakeRetries) +{ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_READ); + + SolidSyslogStream_Open(handle, addr); + CALLED_FUNCTION(NoOpSleep, ONCE); +} + +TEST(SolidSyslogMbedTlsStream, OpenRetriesHandshakeOnWantWrite) +{ + /* WANT_WRITE arises when mbedTLS needs to send (e.g. ClientFinished + * under non-blocking transport with a temporarily-full send buffer). + * Same retry treatment as WANT_READ. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_WRITE); + + CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); + CALLED_FAKE(MbedTlsFake_SslHandshake, TWICE); +} + +TEST(SolidSyslogMbedTlsStream, OpenFailsWhenHandshakeNeverCompletes) +{ + /* mbedtls_ssl_handshake always returns WANT_READ — handshake never makes + * progress, so the bounded budget should expire and Open returns false. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_WANT_READ); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); +} + +TEST(SolidSyslogMbedTlsStream, OpenFailsImmediatelyOnHardSslError) +{ + /* Non-WANT error (e.g. a verify/connection failure) is fail-fast — no + * retry budget burn, no Sleep. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_BAD_INPUT_DATA); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CALLED_FAKE(MbedTlsFake_SslHandshake, ONCE); + CALLED_FUNCTION(NoOpSleep, NEVER); +} + TEST(SolidSyslogMbedTlsStream, SendForwardsBufferToSslWrite) { const unsigned char payload[] = {0x10, 0x20, 0x30}; diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index f8c96802..07de3d2f 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -39,9 +39,16 @@ static mbedtls_ssl_recv_t* lastSslSetBioRecvCallback; static mbedtls_ssl_recv_timeout_t* lastSslSetBioRecvTimeoutCallback; /* mbedtls_ssl_handshake */ +enum +{ + MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS = 8 +}; + static int sslHandshakeCallCount; static mbedtls_ssl_context* lastSslHandshakeArg; static int sslHandshakeReturn; +static int sslHandshakeReturnSequence[MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS]; +static int sslHandshakeReturnSequenceLen; /* mbedtls_ssl_write */ static int sslWriteCallCount; @@ -120,6 +127,7 @@ void MbedTlsFake_Reset(void) sslHandshakeCallCount = 0; lastSslHandshakeArg = NULL; sslHandshakeReturn = 0; + sslHandshakeReturnSequenceLen = 0; sslWriteCallCount = 0; lastSslWriteContextArg = NULL; lastSslWriteBufArg = NULL; @@ -253,6 +261,16 @@ void MbedTlsFake_SetSslHandshakeReturn(int value) sslHandshakeReturn = value; } +void MbedTlsFake_SetSslHandshakeReturnSequence(const int* values, int count) +{ + int safe = (count < MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS) ? count : MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS; + for (int i = 0; i < safe; i++) + { + sslHandshakeReturnSequence[i] = values[i]; + } + sslHandshakeReturnSequenceLen = safe; +} + int MbedTlsFake_SslWriteCallCount(void) { return sslWriteCallCount; @@ -458,9 +476,16 @@ void mbedtls_ssl_set_bio( int mbedtls_ssl_handshake(mbedtls_ssl_context* ssl) { + int callIndex = sslHandshakeCallCount; sslHandshakeCallCount++; lastSslHandshakeArg = ssl; - return sslHandshakeReturn; + int rc = sslHandshakeReturn; + if (sslHandshakeReturnSequenceLen > 0) + { + int idx = (callIndex < sslHandshakeReturnSequenceLen) ? callIndex : (sslHandshakeReturnSequenceLen - 1); + rc = sslHandshakeReturnSequence[idx]; + } + return rc; } int mbedtls_ssl_write(mbedtls_ssl_context* ssl, const unsigned char* buf, size_t len) diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index 78d3339e..d6a16213 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -48,6 +48,11 @@ EXTERN_C_BEGIN int MbedTlsFake_SslHandshakeCallCount(void); struct mbedtls_ssl_context* MbedTlsFake_LastSslHandshakeArg(void); void MbedTlsFake_SetSslHandshakeReturn(int value); + /* Per-call return sequence — each handshake invocation gets the next + * value in `values`; once exhausted, every subsequent call returns the + * last entry. Used to drive WANT_READ/WANT_WRITE retry loops. Capped + * at MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS (silently truncated). */ + void MbedTlsFake_SetSslHandshakeReturnSequence(const int* values, int count); /* mbedtls_ssl_write */ int MbedTlsFake_SslWriteCallCount(void); From bc5786f13091107cd25ae3605e430a407f0a046d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 10:35:05 +0000 Subject: [PATCH 06/13] feat: S08.07 slice 5 MbedTlsStream mTLS + integration scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open now calls mbedtls_ssl_conf_own_cert when the integrator supplies both ClientCertChain and ClientKey on the config — wiring the client identity that mbedTLS sends in response to the server's CertificateRequest. Either pointer NULL means "server-auth only" and the call is skipped, leaving mbedTLS's default of no client cert. Test surface: - MbedTlsFake gains conf_own_cert (counter + last-args). Three new unit tests pin the wiring: wires-when-both-set, skips-when-chain-null, skips-when-key-null. - MbedTlsTestServer learns TrustedClientCa — when non-NULL, server switches to VERIFY_REQUIRED + conf_ca_chain so it requires and verifies a client cert against that CA. - Three new integration tests mirror the OpenSSL TlsStream mTLS scenarios: handshake succeeds with trusted client CA, rejected when client sends no cert, rejected when client cert signed by untrusted CA. Harness: bound both socketpair fds with SO_RCVTIMEO=5s. The negative mTLS paths (no-cert in particular) hit a different cross-thread block than the slice-3 server-cert rejections; rather than chase per-scenario TLS message-order specifics, the cap matches the production handshake retry budget and bounds any test-only deadlock. OpenSSL's harness sidesteps this with an in-memory BIO pair — same goal, different mechanism. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 4 + .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 55 +++++++ Tests/MbedTlsIntegration/MbedTlsTestServer.c | 14 +- Tests/MbedTlsIntegration/MbedTlsTestServer.h | 3 + ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 142 ++++++++++++++++++ Tests/Support/MbedTlsFake.c | 39 +++++ Tests/Support/MbedTlsFake.h | 6 + 7 files changed, 260 insertions(+), 3 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 29b0886f..4cc6a2b5 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -86,6 +86,10 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); + if ((self->Config.ClientCertChain != NULL) && (self->Config.ClientKey != NULL)) + { + (void) mbedtls_ssl_conf_own_cert(&self->SslConfig, self->Config.ClientCertChain, self->Config.ClientKey); + } } if (ok) { diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 7cd1b151..d6289603 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -454,3 +454,58 @@ TEST(SolidSyslogMbedTlsStream, OpenSkipsHostnameWhenServerNameIsNull) LONGS_EQUAL(0, MbedTlsFake_SslSetHostnameCallCount()); } + +/* ------------------------------------------------------------------------- + * mTLS client identity wiring. When the integrator supplies both a + * ClientCertChain and a ClientKey, Open must call mbedtls_ssl_conf_own_cert + * so the client presents its cert during the handshake. Either pointer + * being NULL means "server-auth only" — skip the wiring. + * ------------------------------------------------------------------------- */ + +TEST(SolidSyslogMbedTlsStream, OpenWiresOwnCertWhenClientCertAndKeyProvided) +{ + static mbedtls_x509_crt clientCertMarker; + static mbedtls_pk_context clientKeyMarker; + config.ClientCertChain = &clientCertMarker; + config.ClientKey = &clientKeyMarker; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslConfOwnCertCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslConfOwnCertConfigArg()); + POINTERS_EQUAL(&clientCertMarker, MbedTlsFake_LastSslConfOwnCertCertArg()); + POINTERS_EQUAL(&clientKeyMarker, MbedTlsFake_LastSslConfOwnCertKeyArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenSkipsOwnCertWhenClientCertChainIsNull) +{ + /* Key provided, cert NULL — caller hasn't fully opted in to mTLS, so + * the adapter must not tell mbedTLS anything. setup() leaves + * ClientCertChain at NULL; supplying just a Key is the incomplete case. */ + static mbedtls_pk_context clientKeyMarker; + config.ClientKey = &clientKeyMarker; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(0, MbedTlsFake_SslConfOwnCertCallCount()); +} + +TEST(SolidSyslogMbedTlsStream, OpenSkipsOwnCertWhenClientKeyIsNull) +{ + /* Cert provided, key NULL — still incomplete; same skip. */ + static mbedtls_x509_crt clientCertMarker; + config.ClientCertChain = &clientCertMarker; + ReCreateHandleWithUpdatedConfig(); + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(0, MbedTlsFake_SslConfOwnCertCallCount()); +} diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.c b/Tests/MbedTlsIntegration/MbedTlsTestServer.c index b8ab4d82..7b03202c 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestServer.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.c @@ -57,9 +57,17 @@ struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServe * worker exits cleanly. */ mbedtls_ssl_conf_max_tls_version(&self->SslConfig, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, config->Rng); - /* Server-auth-only: don't require a client cert in slice 3 (mTLS lands - * in slice 5). */ - mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_NONE); + if (config->TrustedClientCa != NULL) + { + /* mTLS: server requires + verifies a client cert against the supplied CA. */ + mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&self->SslConfig, (mbedtls_x509_crt*) &config->TrustedClientCa->Cert, NULL); + } + else + { + /* Server-auth only — no client cert requested. */ + mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_NONE); + } mbedtls_ssl_conf_own_cert( &self->SslConfig, (mbedtls_x509_crt*) &config->ServerCert->Cert, diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.h b/Tests/MbedTlsIntegration/MbedTlsTestServer.h index bc9108dc..1f43ba51 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestServer.h +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.h @@ -18,6 +18,9 @@ EXTERN_C_BEGIN int ServerFd; /* one end of a socketpair; ownership transferred to the server */ const struct MbedTlsTestCert* ServerCert; /* server's cert + matching key */ mbedtls_ctr_drbg_context* Rng; /* shared with the test fixture */ + /* Non-NULL switches the server to require + verify a client cert + * against this CA — drives the mTLS scenarios. NULL = server-auth only. */ + const struct MbedTlsTestCert* TrustedClientCa; }; /* Spawns a worker thread that drives the server-side handshake and then diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index 62230385..f49bb44d 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -7,6 +7,7 @@ extern "C" #include #include #include +#include #include #include "MbedTlsTestCert.h" @@ -49,6 +50,15 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &entropy, pers, sizeof(pers) - 1U); socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + /* Bound both sides' blocking recv to 5s. The OpenSSL integration + * tests use an in-memory BIO pair that never blocks; our socketpair + * harness inherently can deadlock on negative-path handshakes (one + * side waits for a message the other won't send). The cap matches + * the production handshake retry budget — generous for any real + * handshake (sub-second), tight enough that a stuck test fails fast. */ + struct timeval rcvTimeout = {5, 0}; + setsockopt(fds[0], SOL_SOCKET, SO_RCVTIMEO, &rcvTimeout, sizeof(rcvTimeout)); + setsockopt(fds[1], SOL_SOCKET, SO_RCVTIMEO, &rcvTimeout, sizeof(rcvTimeout)); struct MbedTlsTestCertConfig caConfig = {}; caConfig.SubjectName = kCaSubject; @@ -84,15 +94,41 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) } struct SolidSyslogStream* StartServerWithCert(const struct MbedTlsTestCert* cert) + { + return StartServerRequiringClientCa(cert, nullptr); + } + + /* mTLS variant: server requires + verifies a client cert against + * `trustedClientCa`. Passing nullptr behaves like StartServerWithCert. */ + struct SolidSyslogStream* StartServerRequiringClientCa( + const struct MbedTlsTestCert* cert, + const struct MbedTlsTestCert* trustedClientCa + ) { struct MbedTlsTestServerConfig serverConfig = {}; serverConfig.ServerFd = fds[1]; serverConfig.ServerCert = cert; serverConfig.Rng = &rng; + serverConfig.TrustedClientCa = trustedClientCa; server = MbedTlsTestServer_Create(&serverConfig); clientTransport = SocketStream_Create(fds[0]); return clientTransport; } + + /* Build a CA + leaf-cert pair signed by it, for the per-test mTLS + * material. Both outputs must be MbedTlsTestCert_Destroy'd by the + * test body before it returns. */ + void CreateClientIdentitySignedBy( + const struct MbedTlsTestCert* signingCa, + struct MbedTlsTestCert* outClientCert + ) + { + struct MbedTlsTestCertConfig leafConfig = {}; + leafConfig.SubjectName = "CN=solidsyslog-test-client"; + leafConfig.IsCa = 0; + leafConfig.Issuer = signingCa; + MbedTlsTestCert_Create(&leafConfig, outClientCert, &rng); + } }; // clang-format on @@ -162,6 +198,112 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMat CHECK_FALSE_TEXT(opened, "client-side handshake must fail when ServerName does not match the cert's SAN"); } +TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeSucceedsWithClientCertSignedByTrustedCa) +{ + /* Build per-test mTLS material: a client CA + a leaf cert signed by it. + * Server is told to require + verify client certs against this CA. */ + struct MbedTlsTestCert clientCa = {}; + struct MbedTlsTestCertConfig clientCaConfig = {}; + clientCaConfig.SubjectName = "CN=Test Client CA"; + clientCaConfig.IsCa = 1; + MbedTlsTestCert_Create(&clientCaConfig, &clientCa, &rng); + struct MbedTlsTestCert clientCert = {}; + CreateClientIdentitySignedBy(&clientCa, &clientCert); + + struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &clientCa); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &trustedCa.Cert; + config.ServerName = kServerHostname; + config.ClientCertChain = &clientCert.Cert; + config.ClientKey = &clientCert.Key; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_TRUE_TEXT(opened, "client-side mTLS Open should succeed against a server trusting the client CA"); + CHECK_TRUE_TEXT( + MbedTlsTestServer_JoinAndHandshakeSucceeded(server), + "server-side mTLS handshake should mirror the client's success" + ); + + MbedTlsTestCert_Destroy(&clientCert); + MbedTlsTestCert_Destroy(&clientCa); +} + +TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientSendsNoCert) +{ + /* Server requires a client cert but the integrator hasn't opted in to + * mTLS — ClientCertChain / ClientKey are NULL. Server-side verify must + * fail and the client's Open must return false. */ + struct MbedTlsTestCert clientCa = {}; + struct MbedTlsTestCertConfig clientCaConfig = {}; + clientCaConfig.SubjectName = "CN=Test Client CA"; + clientCaConfig.IsCa = 1; + MbedTlsTestCert_Create(&clientCaConfig, &clientCa, &rng); + + struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &clientCa); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &trustedCa.Cert; + config.ServerName = kServerHostname; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_FALSE_TEXT(opened, "mTLS handshake must fail when the client does not present a cert"); + + MbedTlsTestCert_Destroy(&clientCa); +} + +TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientCertSignedByUntrustedCa) +{ + /* Client cert is signed by a CA the server doesn't trust. Server-side + * chain validation fails and Open returns false on the client. */ + struct MbedTlsTestCert trustedClientCa = {}; + struct MbedTlsTestCertConfig trustedConfig = {}; + trustedConfig.SubjectName = "CN=Trusted Client CA"; + trustedConfig.IsCa = 1; + MbedTlsTestCert_Create(&trustedConfig, &trustedClientCa, &rng); + + struct MbedTlsTestCert untrustedClientCa = {}; + struct MbedTlsTestCertConfig untrustedConfig = {}; + untrustedConfig.SubjectName = "CN=Untrusted Client CA"; + untrustedConfig.IsCa = 1; + MbedTlsTestCert_Create(&untrustedConfig, &untrustedClientCa, &rng); + + /* Client cert is signed by the *untrusted* CA — server only trusts + * trustedClientCa, so verify will reject this chain. */ + struct MbedTlsTestCert clientCert = {}; + CreateClientIdentitySignedBy(&untrustedClientCa, &clientCert); + + struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &trustedClientCa); + struct SolidSyslogMbedTlsStreamConfig config = {}; + config.Transport = transport; + config.Sleep = NoOpSleep; + config.Rng = &rng; + config.CaChain = &trustedCa.Cert; + config.ServerName = kServerHostname; + config.ClientCertChain = &clientCert.Cert; + config.ClientKey = &clientCert.Key; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_FALSE_TEXT(opened, "mTLS handshake must fail when the client cert chains to an untrusted CA"); + + MbedTlsTestCert_Destroy(&clientCert); + MbedTlsTestCert_Destroy(&untrustedClientCa); + MbedTlsTestCert_Destroy(&trustedClientCa); +} + TEST(SolidSyslogMbedTlsStreamIntegration, BinaryLinksAgainstRealLibMbedTls) { /* mbedtls_version_get_number() is a constant, side-effect-free symbol diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index 07de3d2f..ff6ef809 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -100,6 +100,12 @@ static int sslSetHostnameCallCount; static mbedtls_ssl_context* lastSslSetHostnameContextArg; static const char* lastSslSetHostnameNameArg; +/* mbedtls_ssl_conf_own_cert */ +static int sslConfOwnCertCallCount; +static mbedtls_ssl_config* lastSslConfOwnCertConfigArg; +static mbedtls_x509_crt* lastSslConfOwnCertCertArg; +static mbedtls_pk_context* lastSslConfOwnCertKeyArg; + /* ------------------------------------------------------------------------- * Test accessors. * ------------------------------------------------------------------------- */ @@ -159,6 +165,10 @@ void MbedTlsFake_Reset(void) sslSetHostnameCallCount = 0; lastSslSetHostnameContextArg = NULL; lastSslSetHostnameNameArg = NULL; + sslConfOwnCertCallCount = 0; + lastSslConfOwnCertConfigArg = NULL; + lastSslConfOwnCertCertArg = NULL; + lastSslConfOwnCertKeyArg = NULL; } int MbedTlsFake_SslConfigInitCallCount(void) @@ -422,6 +432,26 @@ const char* MbedTlsFake_LastSslSetHostnameNameArg(void) return lastSslSetHostnameNameArg; } +int MbedTlsFake_SslConfOwnCertCallCount(void) +{ + return sslConfOwnCertCallCount; +} + +mbedtls_ssl_config* MbedTlsFake_LastSslConfOwnCertConfigArg(void) +{ + return lastSslConfOwnCertConfigArg; +} + +mbedtls_x509_crt* MbedTlsFake_LastSslConfOwnCertCertArg(void) +{ + return lastSslConfOwnCertCertArg; +} + +mbedtls_pk_context* MbedTlsFake_LastSslConfOwnCertKeyArg(void) +{ + return lastSslConfOwnCertKeyArg; +} + /* ------------------------------------------------------------------------- * Link-interposed mbedTLS symbols. The test executable does not link * libmbedtls; the production code's calls to mbedtls_* resolve here. @@ -541,6 +571,15 @@ void mbedtls_ssl_conf_ca_chain(mbedtls_ssl_config* conf, mbedtls_x509_crt* ca_ch lastSslConfCaChainCrlArg = ca_crl; } +int mbedtls_ssl_conf_own_cert(mbedtls_ssl_config* conf, mbedtls_x509_crt* own_cert, mbedtls_pk_context* pk_key) +{ + sslConfOwnCertCallCount++; + lastSslConfOwnCertConfigArg = conf; + lastSslConfOwnCertCertArg = own_cert; + lastSslConfOwnCertKeyArg = pk_key; + return 0; +} + void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, RngFunc f_rng, void* p_rng) { sslConfRngCallCount++; diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index d6a16213..b1e62d12 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -102,6 +102,12 @@ EXTERN_C_BEGIN struct mbedtls_ssl_context* MbedTlsFake_LastSslSetHostnameContextArg(void); const char* MbedTlsFake_LastSslSetHostnameNameArg(void); + /* mbedtls_ssl_conf_own_cert (mTLS client identity wiring) */ + int MbedTlsFake_SslConfOwnCertCallCount(void); + struct mbedtls_ssl_config* MbedTlsFake_LastSslConfOwnCertConfigArg(void); + struct mbedtls_x509_crt* MbedTlsFake_LastSslConfOwnCertCertArg(void); + struct mbedtls_pk_context* MbedTlsFake_LastSslConfOwnCertKeyArg(void); + EXTERN_C_END #endif /* MBEDTLSFAKE_H */ From 5a4cf617102d66ca4ee3302601dc973d257b516b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:03:37 +0000 Subject: [PATCH 07/13] fix: S08.07 MbedTlsStream BioRecv would-block returns WANT_READ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Stream contract uses 0 to signal would-block (mirrored in SolidSyslogTlsStream's TransportBioRead at OpenSsl/Source/SolidSyslogTlsStream.c:262-281). MbedTlsStream_BioRecv was collapsing it to -1, which mbedTLS treats as a fatal transport error — the slice-4 handshake retry loop never got a chance to engage. Real non-blocking transports (Posix/Windows TCP, FreeRTOS-Plus-TCP) would abort the handshake on the first scheduling-induced gap. Hidden today by the integration test harness using blocking socketpair recv with a 5s SO_RCVTIMEO that surfaces as -1 → fatal. With this fix, future non-blocking integration scaffolding would actually drive the retry loop end-to-end. BioSend has the same shape conceptually but the Stream contract gives Send no would-block channel (returns bool), so the symmetric fix isn't possible without extending the Stream API — same as the OpenSSL precedent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 10 +++++ .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 4cc6a2b5..2845f3f7 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -156,6 +156,12 @@ static int MbedTlsStream_BioSend(void* ctx, const unsigned char* buf, size_t len return SolidSyslogStream_Send(self->Config.Transport, buf, len) ? (int) len : -1; } +/* Translate the non-blocking transport's Read contract into mbedTLS's BIO + * recv contract: + * transport > 0 → bytes available, return the same positive count. + * transport = 0 → would-block. Must return MBEDTLS_ERR_SSL_WANT_READ so + * the handshake retry loop polls; returning 0 or -1 would + * abort the handshake on the first non-blocking read. */ static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len) { struct SolidSyslogMbedTlsStream* self = (struct SolidSyslogMbedTlsStream*) ctx; @@ -165,6 +171,10 @@ static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len) { result = (int) n; } + else if (n == 0) + { + result = MBEDTLS_ERR_SSL_WANT_READ; + } return result; } diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index d6289603..2884be6b 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -385,6 +385,43 @@ TEST(SolidSyslogMbedTlsStream, BioRecvCallbackForwardsBufferToTransport) LONGS_EQUAL(sizeof(buffer), StreamFake_LastReadSize(transport)); } +TEST(SolidSyslogMbedTlsStream, BioRecvReturnsWantReadWhenTransportWouldBlock) +{ + /* Stream contract: transport Read returns 0 to signal would-block. mbedTLS + * needs MBEDTLS_ERR_SSL_WANT_READ to drive its retry loop; any other + * negative is fatal. Returning -1 (or 0) here would abort the handshake + * on the first non-blocking poll. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); + void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); + unsigned char buffer[16]; + StreamFake_SetReadReturn(transport, 0); + + int rc = bioRecv(bioContext, buffer, sizeof(buffer)); + + LONGS_EQUAL(MBEDTLS_ERR_SSL_WANT_READ, rc); +} + +TEST(SolidSyslogMbedTlsStream, BioRecvReturnsFatalWhenTransportFails) +{ + /* Stream contract: negative is fatal. mbedTLS treats any negative other + * than its own WANT_* sentinels as a transport error and aborts. */ + SolidSyslogAddressStorage storage = {}; + struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); + SolidSyslogStream_Open(handle, addr); + auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); + void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); + unsigned char buffer[16]; + StreamFake_SetReadReturn(transport, -1); + + int rc = bioRecv(bioContext, buffer, sizeof(buffer)); + + CHECK_TRUE(rc < 0); + CHECK_FALSE(rc == MBEDTLS_ERR_SSL_WANT_READ); +} + TEST(SolidSyslogMbedTlsStream, OpenSetsAuthmodeRequired) { SolidSyslogAddressStorage storage = {}; From 26cc5e5054e0432098a3169a42f95d102ddbeb4f Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:13:40 +0000 Subject: [PATCH 08/13] refactor: S08.07 MbedTlsStream lifecycle + fail-fast Send/Read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the OpenSSL TlsStream shape more closely: - Initialise eagerly calls mbedtls_ssl_init / mbedtls_ssl_config_init so the structs are always in a free-safe state. Drops the IsOpen flag and the close-time guard it required. mbedTLS guarantees a freed struct is left in the same zeroed state init produces, so re-Open after Close works without re-init. - Drop the misleading "*Base = *NullStream_Get()" seed in Initialise; all four vtable slots are immediately overridden, so the NullStream copy was wasted work. - Open is now a chained && of small named helpers (ApplySslConfigDefaults / ApplyTlsPolicy / BindContextToConfig / ConfigureExpectedHostname / InstallTransportCallbacks / PerformHandshake), same shape as OpenSSL's TlsStream_Open. - Send and Read close internally on hard error, matching OpenSSL's fail-fast contract that drives the StreamSender reconnect path. Five new tests pin the contract: SendClosesSslAndTransportOnWriteFailure, SendClosesSsl…OnShortWrite, ReadClosesSsl…OnHardError, ReadDoesNotCloseOnWantRead, CloseAfterInternalCloseFromSendFailureDoesNotDoubleFree. Also fixes the agent-flagged IsOpen ordering bug: previously IsOpen was set true before mbedtls_ssl_init, so a config_defaults failure left Close calling ssl_free on a context that had never been init'd. Eager init removes the dependency on flag timing entirely. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 125 ++++++++++++------ .../Source/SolidSyslogMbedTlsStreamPrivate.h | 5 - .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 73 ++++++++++ 3 files changed, 156 insertions(+), 47 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 2845f3f7..dce6e728 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -24,6 +24,11 @@ struct SolidSyslogAddress; static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base); static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); +static inline bool MbedTlsStream_ApplySslConfigDefaults(struct SolidSyslogMbedTlsStream* self); +static inline void MbedTlsStream_ApplyTlsPolicy(struct SolidSyslogMbedTlsStream* self); +static inline bool MbedTlsStream_BindContextToConfig(struct SolidSyslogMbedTlsStream* self); +static inline bool MbedTlsStream_ConfigureExpectedHostname(struct SolidSyslogMbedTlsStream* self); +static inline void MbedTlsStream_InstallTransportCallbacks(struct SolidSyslogMbedTlsStream* self); static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStream* self); static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc); static inline bool MbedTlsStream_IsHandshakeBudgetExhausted(int totalSleptMs); @@ -36,22 +41,24 @@ static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len); void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); - /* Read / Close inherit NullStream defaults until slice-2 tests force - * their real implementations. */ - self->Base = *SolidSyslogNullStream_Get(); self->Base.Open = MbedTlsStream_Open; self->Base.Send = MbedTlsStream_Send; self->Base.Read = MbedTlsStream_Read; self->Base.Close = MbedTlsStream_Close; self->Config = *config; - self->IsOpen = false; + /* Eager init so mbedtls_*_free in Close is always safe — whether Open + * was ever reached, whether it succeeded, or whether Close is being + * called twice in a row. mbedTLS guarantees a freed struct is left in + * the same zeroed state an init produces, so re-Open after Close also + * works without re-init. */ + mbedtls_ssl_init(&self->SslContext); + mbedtls_ssl_config_init(&self->SslConfig); } void MbedTlsStream_Cleanup(struct SolidSyslogStream* base) { /* Mirror the OpenSSL TlsStream pattern: an integrator who destroys a - * still-Open stream must not leak the underlying TLS state. Close - * guards on IsOpen so Cleanup is safe even when Open was never reached. */ + * still-Open stream must not leak the underlying TLS state. */ MbedTlsStream_Close(base); /* Overwrite the abstract base with the shared NullStream vtable so * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ @@ -66,48 +73,63 @@ static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); - bool ok = SolidSyslogStream_Open(self->Config.Transport, addr); + bool ok = SolidSyslogStream_Open(self->Config.Transport, addr) && MbedTlsStream_ApplySslConfigDefaults(self); if (ok) { - mbedtls_ssl_config_init(&self->SslConfig); - /* Flip IsOpen now so any subsequent allocation failure in this - * function still leaves the struct in a Close-safe state — the - * partially-initialised config/context must reach mbedtls_*_free. */ - self->IsOpen = true; - ok = mbedtls_ssl_config_defaults( - &self->SslConfig, - MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT - ) == 0; + MbedTlsStream_ApplyTlsPolicy(self); + ok = MbedTlsStream_BindContextToConfig(self) && MbedTlsStream_ConfigureExpectedHostname(self); } if (ok) { - mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); - mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); - if ((self->Config.ClientCertChain != NULL) && (self->Config.ClientKey != NULL)) - { - (void) mbedtls_ssl_conf_own_cert(&self->SslConfig, self->Config.ClientCertChain, self->Config.ClientKey); - } + MbedTlsStream_InstallTransportCallbacks(self); + ok = MbedTlsStream_PerformHandshake(self); } - if (ok) + return ok; +} + +static inline bool MbedTlsStream_ApplySslConfigDefaults(struct SolidSyslogMbedTlsStream* self) +{ + return mbedtls_ssl_config_defaults( + &self->SslConfig, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ) == 0; +} + +/* TLS policy owned by the library — set per-ssl_config so it cannot leak + * into the integrator's other ssl_configs (per coexistence contract). */ +static inline void MbedTlsStream_ApplyTlsPolicy(struct SolidSyslogMbedTlsStream* self) +{ + mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); + mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); + if ((self->Config.ClientCertChain != NULL) && (self->Config.ClientKey != NULL)) { - mbedtls_ssl_init(&self->SslContext); - ok = mbedtls_ssl_setup(&self->SslContext, &self->SslConfig) == 0; + (void) mbedtls_ssl_conf_own_cert(&self->SslConfig, self->Config.ClientCertChain, self->Config.ClientKey); } - if (ok && (self->Config.ServerName != NULL)) +} + +static inline bool MbedTlsStream_BindContextToConfig(struct SolidSyslogMbedTlsStream* self) +{ + return mbedtls_ssl_setup(&self->SslContext, &self->SslConfig) == 0; +} + +static inline bool MbedTlsStream_ConfigureExpectedHostname(struct SolidSyslogMbedTlsStream* self) +{ + bool ok = true; + if (self->Config.ServerName != NULL) { ok = mbedtls_ssl_set_hostname(&self->SslContext, self->Config.ServerName) == 0; } - if (ok) - { - mbedtls_ssl_set_bio(&self->SslContext, self, MbedTlsStream_BioSend, MbedTlsStream_BioRecv, NULL); - ok = MbedTlsStream_PerformHandshake(self); - } return ok; } +static inline void MbedTlsStream_InstallTransportCallbacks(struct SolidSyslogMbedTlsStream* self) +{ + mbedtls_ssl_set_bio(&self->SslContext, self, MbedTlsStream_BioSend, MbedTlsStream_BioRecv, NULL); +} + /* Drive mbedtls_ssl_handshake to completion under non-blocking transport. * Each call may return WANT_READ/WANT_WRITE while waiting for the multi-RTT * handshake to progress; we sleep briefly between attempts (avoiding a busy @@ -178,13 +200,28 @@ static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len) return result; } +/* TLS-level write failure means the session state is unrecoverable — close + * so the StreamSender reconnect path runs on the next tick. Mirrors the + * OpenSSL TlsStream_Send fail-fast contract. */ static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); int rc = mbedtls_ssl_write(&self->SslContext, (const unsigned char*) buffer, size); - return (rc > 0) && ((size_t) rc == size); + bool ok = (rc > 0) && ((size_t) rc == size); + if (!ok) + { + MbedTlsStream_Close(base); + } + return ok; } +/* mbedtls_ssl_read has two distinct outcomes worth keeping straight: + * 1. Steady-state read: bytes available → positive count; nothing to read + * right now → WANT_READ → return 0, mirroring the transport contract. + * 2. Any other negative return (alerts, renegotiation surfacing as + * WANT_WRITE, hard transport error) is fatal under fail-fast semantics + * — close internally; the caller reopens and store-and-forward replays. + * Same shape as the OpenSSL TlsStream_Read. */ static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); @@ -198,18 +235,22 @@ static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base { result = 0; } + else + { + MbedTlsStream_Close(base); + } return result; } +/* Idempotent: a previous Close left the structs in mbedTLS's freed-equivalent + * (zeroed) state, so close_notify sees conf == NULL and returns harmlessly, + * and the *_free calls are no-ops on already-freed structs. Transport Close + * is itself idempotent on every Stream impl. */ static inline void MbedTlsStream_Close(struct SolidSyslogStream* base) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); - if (self->IsOpen) - { - (void) mbedtls_ssl_close_notify(&self->SslContext); - mbedtls_ssl_free(&self->SslContext); - mbedtls_ssl_config_free(&self->SslConfig); - self->IsOpen = false; - } + (void) mbedtls_ssl_close_notify(&self->SslContext); + mbedtls_ssl_free(&self->SslContext); + mbedtls_ssl_config_free(&self->SslConfig); SolidSyslogStream_Close(self->Config.Transport); } diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h index 27a28e00..e6661a89 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -2,7 +2,6 @@ #define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H #include -#include #include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogStreamDefinition.h" @@ -13,10 +12,6 @@ struct SolidSyslogMbedTlsStream struct SolidSyslogMbedTlsStreamConfig Config; mbedtls_ssl_config SslConfig; mbedtls_ssl_context SslContext; - /* Tracks whether Open has reached the point of mbedTLS state allocation. - * Close guards its mbedtls_*_free calls on this so an unopened stream - * can be destroyed without touching uninitialised structs. */ - bool IsOpen; }; void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config); diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 2884be6b..d81a22a2 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -281,6 +281,34 @@ TEST(SolidSyslogMbedTlsStream, SendReturnsFalseWhenSslWriteFails) CHECK_FALSE(SolidSyslogStream_Send(handle, payload, sizeof(payload))); } +TEST(SolidSyslogMbedTlsStream, SendClosesSslAndTransportOnWriteFailure) +{ + /* Fail-fast: a TLS-level write failure means the session state is + * unrecoverable. Mirror the OpenSSL TlsStream contract — close internally + * so the StreamSender reconnect path runs on the next tick. */ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + MbedTlsFake_SetSslWriteReturn(-1); + + SolidSyslogStream_Send(handle, payload, sizeof(payload)); + + LONGS_EQUAL(1, MbedTlsFake_SslFreeCallCount()); + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); +} + +TEST(SolidSyslogMbedTlsStream, SendClosesSslAndTransportOnShortWrite) +{ + /* mbedtls_ssl_write returning fewer bytes than requested is treated the + * same as outright failure — the application boundary requires + * all-or-nothing writes (syslog framing). */ + const unsigned char payload[] = {0x10, 0x20, 0x30}; + MbedTlsFake_SetSslWriteReturn(2); /* asked for 3, got 2 */ + + SolidSyslogStream_Send(handle, payload, sizeof(payload)); + + LONGS_EQUAL(1, MbedTlsFake_SslFreeCallCount()); + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); +} + TEST(SolidSyslogMbedTlsStream, ReadForwardsBufferToSslRead) { unsigned char buffer[8]; @@ -317,6 +345,51 @@ TEST(SolidSyslogMbedTlsStream, ReadReturnsNegativeOnSslReadError) CHECK(SolidSyslogStream_Read(handle, buffer, sizeof(buffer)) < 0); } +TEST(SolidSyslogMbedTlsStream, ReadClosesSslAndTransportOnHardError) +{ + /* Same fail-fast contract as Send: any read result other than positive + * bytes or WANT_READ (e.g. peer close_notify, fatal alert, transport + * error) closes internally so the next tick reopens. */ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(-1); + + SolidSyslogStream_Read(handle, buffer, sizeof(buffer)); + + LONGS_EQUAL(1, MbedTlsFake_SslFreeCallCount()); + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); +} + +TEST(SolidSyslogMbedTlsStream, ReadDoesNotCloseOnWantRead) +{ + /* WANT_READ is steady-state would-block, not a connection failure — + * leave the session intact so the caller can retry. */ + unsigned char buffer[8]; + MbedTlsFake_SetSslReadReturn(MBEDTLS_ERR_SSL_WANT_READ); + + SolidSyslogStream_Read(handle, buffer, sizeof(buffer)); + + LONGS_EQUAL(0, MbedTlsFake_SslFreeCallCount()); + LONGS_EQUAL(0, StreamFake_CloseCallCount(transport)); +} + +TEST(SolidSyslogMbedTlsStream, CloseAfterInternalCloseFromSendFailureDoesNotDoubleFree) +{ + /* Send and Read may close internally on failure; the subsequent Close + * from the StreamSender reconnect path or Destroy must not crash or + * double-free. mbedTLS's freed-equivalent zeroed state makes this safe. */ + const unsigned char payload[] = {0x10}; + MbedTlsFake_SetSslWriteReturn(-1); + SolidSyslogStream_Send(handle, payload, sizeof(payload)); /* internal close */ + + SolidSyslogStream_Close(handle); /* second close — must be safe */ + + /* Exactly one of each free per real session. The teardown's Destroy + * will add a third pair when this test ends, but that's outside the + * assertion window. */ + LONGS_EQUAL(2, MbedTlsFake_SslFreeCallCount()); + LONGS_EQUAL(2, MbedTlsFake_SslConfigFreeCallCount()); +} + TEST(SolidSyslogMbedTlsStream, CloseSendsSslCloseNotifyOnTheSslContextFromOpen) { SolidSyslogAddressStorage storage = {}; From 72856d2cc8689e3b90523853aaea3e7280f4fda9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:24:05 +0000 Subject: [PATCH 09/13] chore: S08.07 MbedTlsFake cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expose MbedTlsFake_LastSslSetBioRecvTimeoutCallback accessor and assert it's NULL in OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout. The test name promised the assertion since slice 2; it wasn't being made, and the captured field had nothing reading it. - Drop the file-local RngFunc typedef. Project convention disallows project-owned typedefs (NAMING.md); the accessor header already uses the inline function-pointer form, so just match it in the .c. - NOLINTNEXTLINE on mbedtls_ctr_drbg_random's output parameter — the signature is fixed by the mbedTLS API contract (it's an out-buffer the contract writes to), so readability-non-const-parameter can't apply. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 5 +++++ Tests/Support/MbedTlsFake.c | 13 +++++++++---- Tests/Support/MbedTlsFake.h | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index d81a22a2..b9418705 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -140,6 +140,10 @@ TEST(SolidSyslogMbedTlsStream, OpenSetupBindsSslContextToSslConfig) TEST(SolidSyslogMbedTlsStream, OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout) { + /* mbedTLS's set_bio takes both a recv and a recv_timeout callback; + * we install the former (non-blocking would-block via WANT_READ) and + * leave the latter NULL since we manage timeouts via PerformHandshake's + * Sleep-based budget rather than mbedTLS's internal timer. */ SolidSyslogAddressStorage storage = {}; struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); @@ -150,6 +154,7 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout CHECK(MbedTlsFake_LastSslSetBioPBioArg() != nullptr); CHECK(MbedTlsFake_LastSslSetBioSendCallback() != nullptr); CHECK(MbedTlsFake_LastSslSetBioRecvCallback() != nullptr); + POINTERS_EQUAL(nullptr, (void*) MbedTlsFake_LastSslSetBioRecvTimeoutCallback()); } TEST(SolidSyslogMbedTlsStream, OpenDrivesHandshakeOnTheSslContext) diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index ff6ef809..d30455cb 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -89,10 +89,9 @@ static mbedtls_x509_crt* lastSslConfCaChainArg; static mbedtls_x509_crl* lastSslConfCaChainCrlArg; /* mbedtls_ssl_conf_rng */ -typedef int (*RngFunc)(void*, unsigned char*, size_t); static int sslConfRngCallCount; static mbedtls_ssl_config* lastSslConfRngConfigArg; -static RngFunc lastSslConfRngFuncArg; +static int (*lastSslConfRngFuncArg)(void*, unsigned char*, size_t); static void* lastSslConfRngContextArg; /* mbedtls_ssl_set_hostname */ @@ -256,6 +255,11 @@ mbedtls_ssl_recv_t* MbedTlsFake_LastSslSetBioRecvCallback(void) return lastSslSetBioRecvCallback; } +mbedtls_ssl_recv_timeout_t* MbedTlsFake_LastSslSetBioRecvTimeoutCallback(void) +{ + return lastSslSetBioRecvTimeoutCallback; +} + int MbedTlsFake_SslHandshakeCallCount(void) { return sslHandshakeCallCount; @@ -407,7 +411,7 @@ mbedtls_ssl_config* MbedTlsFake_LastSslConfRngConfigArg(void) return lastSslConfRngConfigArg; } -RngFunc MbedTlsFake_LastSslConfRngFuncArg(void) +int (*MbedTlsFake_LastSslConfRngFuncArg(void))(void*, unsigned char*, size_t) { return lastSslConfRngFuncArg; } @@ -580,7 +584,7 @@ int mbedtls_ssl_conf_own_cert(mbedtls_ssl_config* conf, mbedtls_x509_crt* own_ce return 0; } -void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, RngFunc f_rng, void* p_rng) +void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, int (*f_rng)(void*, unsigned char*, size_t), void* p_rng) { sslConfRngCallCount++; lastSslConfRngConfigArg = conf; @@ -592,6 +596,7 @@ void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, RngFunc f_rng, void* p_rng) * wiring conf_rng. The fake never actually invokes the function via the * captured pointer, so this body never runs at test time. Defined here so the * symbol resolves at link time without pulling in real libmbedcrypto. */ +// NOLINTNEXTLINE(readability-non-const-parameter) -- signature fixed by mbedTLS API; `output` is an out-buffer the contract writes to int mbedtls_ctr_drbg_random(void* p_rng, unsigned char* output, size_t output_len) { (void) p_rng; diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index b1e62d12..84d8dc5e 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -2,6 +2,7 @@ #define MBEDTLSFAKE_H #include +#include #include "ExternC.h" @@ -43,6 +44,7 @@ EXTERN_C_BEGIN void* MbedTlsFake_LastSslSetBioPBioArg(void); int (*MbedTlsFake_LastSslSetBioSendCallback(void))(void*, const unsigned char*, size_t); int (*MbedTlsFake_LastSslSetBioRecvCallback(void))(void*, unsigned char*, size_t); + int (*MbedTlsFake_LastSslSetBioRecvTimeoutCallback(void))(void*, unsigned char*, size_t, uint32_t); /* mbedtls_ssl_handshake */ int MbedTlsFake_SslHandshakeCallCount(void); From 8de7b017e3736171acd03f402bdaa7fe4884e3ee Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:35:07 +0000 Subject: [PATCH 10/13] chore: S08.07 MbedTls integration harness cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MbedTlsTestServer: drop the dead Received[1024] / ReceivedLength plumbing and accessors — no test reads them and RunServer never writes them. Single-return on Destroy (NULL-guard wraps the body instead of early-return). Drop stale "slice 3 only pins" comment. - SocketStream: rename static vtable functions to SocketStream_* prefix per feedback_static_name_prefix. Collapse Destroy to delegate to Close so the idempotent-close logic lives in one place. - MbedTlsTestCert: replace strncpy with SafeString_Copy (link SafeStringStandard.c into the integration target). Collapse WriteCertToDer's 7-parameter signature into a 4-parameter shape using a DerBuffer struct — also clears the bugprone-easily-swappable-parameters tidy error on the size_t* pair. - NOLINT the MbedTestCert-by-MbedTestCert pair on StartServerRequiringClientCa in the integration test (function name distinguishes the roles, and reordering can't help with 2 args). All three MbedTls binaries now pass tidy cleanly (production tidy was already passing); no leaks under ASan; 7 integration + 42 unit + 10 pool tests green. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/MbedTlsIntegration/CMakeLists.txt | 2 + Tests/MbedTlsIntegration/MbedTlsTestCert.c | 51 ++++++++-------- Tests/MbedTlsIntegration/MbedTlsTestServer.c | 58 ++++++------------- Tests/MbedTlsIntegration/MbedTlsTestServer.h | 12 +--- Tests/MbedTlsIntegration/SocketStream.c | 37 +++++------- ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 2 + 6 files changed, 68 insertions(+), 94 deletions(-) diff --git a/Tests/MbedTlsIntegration/CMakeLists.txt b/Tests/MbedTlsIntegration/CMakeLists.txt index 8e94121e..584be079 100644 --- a/Tests/MbedTlsIntegration/CMakeLists.txt +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(MbedTlsIntegrationTests SocketStream.c MbedTlsTestCert.c MbedTlsTestServer.c + ${CMAKE_SOURCE_DIR}/Tests/Support/SafeStringStandard.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c ) @@ -68,6 +69,7 @@ target_include_directories(MbedTlsIntegrationTests PRIVATE ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface ${CMAKE_SOURCE_DIR}/Core/Interface ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests/Support ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.c b/Tests/MbedTlsIntegration/MbedTlsTestCert.c index 79e9d96e..36fcac97 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestCert.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.c @@ -10,6 +10,8 @@ #include #include +#include "SafeString.h" + enum { RSA_KEY_BITS = 2048, @@ -17,15 +19,25 @@ enum DER_BUFFER_BYTES = 4096 }; +/* mbedtls_x509write_crt_der writes from the END of the buffer backwards, + * so the DER blob occupies [Bytes + StartOffset, Bytes + StartOffset + Length). + * Capacity is the underlying storage size. Bundling these four fields keeps + * WriteCertToDer's signature tight and dodges adjacent-size_t-parameter + * swappability hazards. */ +struct DerBuffer +{ + unsigned char* Bytes; + size_t Capacity; + size_t StartOffset; + size_t Length; +}; + static void GenerateKey(mbedtls_pk_context* key, mbedtls_ctr_drbg_context* rng); static void WriteCertToDer( const struct MbedTlsTestCertConfig* config, mbedtls_pk_context* subjectKey, - unsigned char* derBuffer, - size_t bufferSize, - size_t* derStartOffset, - size_t* derLength, - mbedtls_ctr_drbg_context* rng + mbedtls_ctr_drbg_context* rng, + struct DerBuffer* out ); void MbedTlsTestCert_Create( @@ -36,17 +48,15 @@ void MbedTlsTestCert_Create( { mbedtls_pk_init(&out->Key); mbedtls_x509_crt_init(&out->Cert); - strncpy(out->SubjectName, config->SubjectName, sizeof(out->SubjectName) - 1U); - out->SubjectName[sizeof(out->SubjectName) - 1U] = '\0'; + SafeString_Copy(out->SubjectName, sizeof(out->SubjectName), config->SubjectName); GenerateKey(&out->Key, rng); - unsigned char derBuffer[DER_BUFFER_BYTES]; - size_t derStartOffset = 0U; - size_t derLength = 0U; - WriteCertToDer(config, &out->Key, derBuffer, sizeof(derBuffer), &derStartOffset, &derLength, rng); + unsigned char derBytes[DER_BUFFER_BYTES]; + struct DerBuffer der = {derBytes, sizeof(derBytes), 0U, 0U}; + WriteCertToDer(config, &out->Key, rng, &der); - mbedtls_x509_crt_parse_der(&out->Cert, &derBuffer[derStartOffset], derLength); + mbedtls_x509_crt_parse_der(&out->Cert, &der.Bytes[der.StartOffset], der.Length); } void MbedTlsTestCert_Destroy(struct MbedTlsTestCert* cert) @@ -64,11 +74,8 @@ static void GenerateKey(mbedtls_pk_context* key, mbedtls_ctr_drbg_context* rng) static void WriteCertToDer( const struct MbedTlsTestCertConfig* config, mbedtls_pk_context* subjectKey, - unsigned char* derBuffer, - size_t bufferSize, - size_t* derStartOffset, - size_t* derLength, - mbedtls_ctr_drbg_context* rng + mbedtls_ctr_drbg_context* rng, + struct DerBuffer* out ) { mbedtls_x509write_cert crt; @@ -104,14 +111,12 @@ static void WriteCertToDer( mbedtls_x509write_crt_set_subject_alternative_name(&crt, &san); } - /* mbedtls_x509write_crt_der writes at the END of the buffer, returning - * the number of bytes written. The actual DER starts at offset - * (bufferSize - written). */ - int written = mbedtls_x509write_crt_der(&crt, derBuffer, bufferSize, mbedtls_ctr_drbg_random, rng); + /* mbedtls_x509write_crt_der writes from the END of the buffer backwards. */ + int written = mbedtls_x509write_crt_der(&crt, out->Bytes, out->Capacity, mbedtls_ctr_drbg_random, rng); if (written > 0) { - *derStartOffset = bufferSize - (size_t) written; - *derLength = (size_t) written; + out->StartOffset = out->Capacity - (size_t) written; + out->Length = (size_t) written; } mbedtls_x509write_crt_free(&crt); diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.c b/Tests/MbedTlsIntegration/MbedTlsTestServer.c index 7b03202c..484560ca 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestServer.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.c @@ -12,11 +12,6 @@ #include "MbedTlsTestCert.h" -enum -{ - RECEIVE_BUFFER_BYTES = 1024 -}; - struct MbedTlsTestServer { int Fd; @@ -25,8 +20,6 @@ struct MbedTlsTestServer pthread_t Thread; bool ThreadJoined; bool HandshakeSucceeded; - unsigned char Received[RECEIVE_BUFFER_BYTES]; - size_t ReceivedLength; }; static void* RunServer(void* arg); @@ -84,28 +77,27 @@ struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServe void MbedTlsTestServer_Destroy(struct MbedTlsTestServer* self) { - if (self == NULL) - { - return; - } - if (!self->ThreadJoined) + if (self != NULL) { - /* Worker might still be blocked in recv. Shutting the fd unblocks - * it; close gets called below after the join. */ + if (!self->ThreadJoined) + { + /* Worker might still be blocked in recv. Shutting the fd + * unblocks it; close gets called below after the join. */ + if (self->Fd >= 0) + { + shutdown(self->Fd, SHUT_RDWR); + } + pthread_join(self->Thread, NULL); + self->ThreadJoined = true; + } + mbedtls_ssl_free(&self->SslContext); + mbedtls_ssl_config_free(&self->SslConfig); if (self->Fd >= 0) { - shutdown(self->Fd, SHUT_RDWR); + close(self->Fd); } - pthread_join(self->Thread, NULL); - self->ThreadJoined = true; + free(self); } - mbedtls_ssl_free(&self->SslContext); - mbedtls_ssl_config_free(&self->SslConfig); - if (self->Fd >= 0) - { - close(self->Fd); - } - free(self); } bool MbedTlsTestServer_JoinAndHandshakeSucceeded(struct MbedTlsTestServer* self) @@ -121,16 +113,9 @@ bool MbedTlsTestServer_JoinAndHandshakeSucceeded(struct MbedTlsTestServer* self) return self->HandshakeSucceeded; } -const unsigned char* MbedTlsTestServer_ReceivedBytes(struct MbedTlsTestServer* self) -{ - return self->Received; -} - -size_t MbedTlsTestServer_ReceivedLength(struct MbedTlsTestServer* self) -{ - return self->ReceivedLength; -} - +/* The thread exits as soon as the handshake settles — the tests pin + * handshake outcome only. Reading application bytes after handshake (and + * the blocking that implies) is intentionally not implemented. */ static void* RunServer(void* arg) { struct MbedTlsTestServer* self = (struct MbedTlsTestServer*) arg; @@ -142,11 +127,6 @@ static void* RunServer(void* arg) } while ((handshakeRc == MBEDTLS_ERR_SSL_WANT_READ) || (handshakeRc == MBEDTLS_ERR_SSL_WANT_WRITE)); self->HandshakeSucceeded = (handshakeRc == 0); - - /* Slice 3 only pins handshake outcome; the thread exits as soon as the - * handshake settles. Reading TLS bytes from the server side (and any - * blocking that implies) is left for a future slice when a test actually - * needs it. */ return NULL; } diff --git a/Tests/MbedTlsIntegration/MbedTlsTestServer.h b/Tests/MbedTlsIntegration/MbedTlsTestServer.h index 1f43ba51..185357e6 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestServer.h +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.h @@ -3,7 +3,6 @@ #include #include -#include #include "ExternC.h" @@ -23,9 +22,8 @@ EXTERN_C_BEGIN const struct MbedTlsTestCert* TrustedClientCa; }; - /* Spawns a worker thread that drives the server-side handshake and then - * reads bytes until the peer closes. The Server owns ServerFd from this - * point on. */ + /* Spawns a worker thread that drives the server-side handshake. The + * Server owns ServerFd from this point on. */ struct MbedTlsTestServer* MbedTlsTestServer_Create(const struct MbedTlsTestServerConfig* config); /* Joins the worker thread and tears down all mbedTLS state. */ @@ -35,12 +33,6 @@ EXTERN_C_BEGIN * completed successfully. Callable once per server. */ bool MbedTlsTestServer_JoinAndHandshakeSucceeded(struct MbedTlsTestServer * self); - /* After Join: the bytes the server read via mbedtls_ssl_read after a - * successful handshake (zero-length if the handshake failed or the peer - * closed without sending). */ - const unsigned char* MbedTlsTestServer_ReceivedBytes(struct MbedTlsTestServer * self); - size_t MbedTlsTestServer_ReceivedLength(struct MbedTlsTestServer * self); - EXTERN_C_END #endif /* MBEDTLSTESTSERVER_H */ diff --git a/Tests/MbedTlsIntegration/SocketStream.c b/Tests/MbedTlsIntegration/SocketStream.c index 86fb1739..e199915c 100644 --- a/Tests/MbedTlsIntegration/SocketStream.c +++ b/Tests/MbedTlsIntegration/SocketStream.c @@ -16,38 +16,31 @@ struct SocketStream int Fd; }; -static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); -static void Close(struct SolidSyslogStream* self); +static bool SocketStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool SocketStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static SolidSyslogSsize SocketStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void SocketStream_Close(struct SolidSyslogStream* self); struct SolidSyslogStream* SocketStream_Create(int fd) { struct SocketStream* stream = (struct SocketStream*) calloc(1, sizeof(struct SocketStream)); - stream->Base.Open = Open; - stream->Base.Send = Send; - stream->Base.Read = Read; - stream->Base.Close = Close; + stream->Base.Open = SocketStream_Open; + stream->Base.Send = SocketStream_Send; + stream->Base.Read = SocketStream_Read; + stream->Base.Close = SocketStream_Close; stream->Fd = fd; return &stream->Base; } void SocketStream_Destroy(struct SolidSyslogStream* self) { - struct SocketStream* stream = (struct SocketStream*) self; - /* Idempotent close: if the production Close path already ran it will - * have set Fd to -1. Otherwise (e.g. handshake failure where Close - * never runs) we still own the fd and must release it so the server's - * blocking recv unblocks promptly. */ - if (stream->Fd >= 0) - { - close(stream->Fd); - stream->Fd = -1; - } + /* Idempotent: SocketStream_Close handles the fd lifecycle and sets Fd + * to -1 so this stays safe whether production Close ran first or not. */ + SocketStream_Close(self); free(self); } -static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +static bool SocketStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { /* The fd is pre-connected (e.g. socketpair); Open is a no-op so the test * harness fully controls connection lifecycle. */ @@ -56,7 +49,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress return true; } -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +static bool SocketStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size) { struct SocketStream* stream = (struct SocketStream*) self; const unsigned char* bytes = (const unsigned char*) buffer; @@ -74,14 +67,14 @@ static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size return true; } -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) +static SolidSyslogSsize SocketStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) { struct SocketStream* stream = (struct SocketStream*) self; ssize_t n = recv(stream->Fd, buffer, size, 0); return (SolidSyslogSsize) n; } -static void Close(struct SolidSyslogStream* self) +static void SocketStream_Close(struct SolidSyslogStream* self) { struct SocketStream* stream = (struct SocketStream*) self; if (stream->Fd >= 0) diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index f49bb44d..49264f17 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -100,10 +100,12 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) /* mTLS variant: server requires + verifies a client cert against * `trustedClientCa`. Passing nullptr behaves like StartServerWithCert. */ + // NOLINTBEGIN(bugprone-easily-swappable-parameters) -- both args are MbedTlsTestCert by domain; the function name distinguishes their roles struct SolidSyslogStream* StartServerRequiringClientCa( const struct MbedTlsTestCert* cert, const struct MbedTlsTestCert* trustedClientCa ) + // NOLINTEND(bugprone-easily-swappable-parameters) { struct MbedTlsTestServerConfig serverConfig = {}; serverConfig.ServerFd = fds[1]; From 73e8d2dc525117bcd9ad5501f6238eddce4f96d5 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:55:31 +0000 Subject: [PATCH 11/13] chore: S08.07 MbedTls test cleanup pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lift addr/addrStorage into the unit-test TEST_GROUP fixture (mirrors the OpenSSL TlsStream test); collapses ~25 three-line prologues. - Rename OpenInitialisesSslContext / OpenInitialisesSslConfig to CreateInitialisesSslContextForSafeFree / …SslConfigForSafeFree. The init now happens eagerly in Initialise (called from Create) so the symmetric *_free in Close is safe even if Open is never reached; the old names misdescribed which lifecycle step actually exercises it. Rename OpenSetupBindsSslContextToSslConfig to OpenBindsContextToConfig to drop the doubled mechanism noun. - Lift unit-test file-scope NoOpSleepCallCount / g_lastSleepMs (and the NoOpSleep callback) out of an anonymous namespace into bare static, matching the OpenSSL TlsStream test's idiom. - Extract BuildBaseConfig helper in the integration test; collapses the 6 identical "transport / sleep / rng / caChain / serverName" stanzas. - Fix SocketStream_Read EOF/would-block conflation in the integration harness. recv()==0 means peer-closed; the Stream contract reserves 0 for would-block (which BioRecv now maps to MBEDTLS_ERR_SSL_WANT_READ, per slice-4-followup). Forwarding a real EOF as 0 would loop the handshake until the 5s budget expired. Map EOF to -1 instead. --- .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 104 +++--------------- Tests/MbedTlsIntegration/SocketStream.c | 12 +- ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 56 ++++------ 3 files changed, 50 insertions(+), 122 deletions(-) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index b9418705..7eb61efc 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -17,24 +17,23 @@ extern "C" using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER/TWICE into scope for CALLED_FUNCTION / CALLED_FAKE -namespace -{ -int NoOpSleepCallCount; -int g_lastSleepMs; +static int NoOpSleepCallCount; +static int g_lastSleepMs; -void NoOpSleep(int milliseconds) +static void NoOpSleep(int milliseconds) { NoOpSleepCallCount++; g_lastSleepMs = milliseconds; } -} // namespace // clang-format off TEST_GROUP(SolidSyslogMbedTlsStream) { - struct SolidSyslogStream* transport = nullptr; - struct SolidSyslogStream* handle = nullptr; - struct SolidSyslogMbedTlsStreamConfig config = {}; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogStream* handle = nullptr; + struct SolidSyslogMbedTlsStreamConfig config = {}; + SolidSyslogAddressStorage addrStorage = {}; + struct SolidSyslogAddress* addr = nullptr; void setup() override { @@ -45,6 +44,8 @@ TEST_GROUP(SolidSyslogMbedTlsStream) config.Transport = transport; config.Sleep = NoOpSleep; handle = SolidSyslogMbedTlsStream_Create(&config); + // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros + addr = SolidSyslogAddress_FromStorage(&addrStorage); } void teardown() override @@ -83,30 +84,22 @@ TEST_GROUP(SolidSyslogMbedTlsStream) TEST(SolidSyslogMbedTlsStream, OpenDelegatesToInjectedTransport) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, StreamFake_OpenCallCount(transport)); POINTERS_EQUAL(addr, StreamFake_LastOpenAddr(transport)); } -TEST(SolidSyslogMbedTlsStream, OpenInitialisesSslConfig) +TEST(SolidSyslogMbedTlsStream, CreateInitialisesSslConfigForSafeFree) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - - SolidSyslogStream_Open(handle, addr); - + /* Init happens eagerly in Create (via MbedTlsStream_Initialise) so the + * symmetric *_free in Close is always safe — whether Open was reached, + * whether it succeeded, or whether Close is called more than once. */ LONGS_EQUAL(1, MbedTlsFake_SslConfigInitCallCount()); } TEST(SolidSyslogMbedTlsStream, OpenAppliesClientStreamDefaultsToSslConfig) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslConfigDefaultsCallCount()); @@ -116,21 +109,14 @@ TEST(SolidSyslogMbedTlsStream, OpenAppliesClientStreamDefaultsToSslConfig) LONGS_EQUAL(MBEDTLS_SSL_PRESET_DEFAULT, MbedTlsFake_LastSslConfigDefaultsPreset()); } -TEST(SolidSyslogMbedTlsStream, OpenInitialisesSslContext) +TEST(SolidSyslogMbedTlsStream, CreateInitialisesSslContextForSafeFree) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - - SolidSyslogStream_Open(handle, addr); - + /* Same eager-init invariant as the SslConfig case above. */ LONGS_EQUAL(1, MbedTlsFake_SslInitCallCount()); } -TEST(SolidSyslogMbedTlsStream, OpenSetupBindsSslContextToSslConfig) +TEST(SolidSyslogMbedTlsStream, OpenBindsContextToConfig) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslSetupCallCount()); @@ -144,9 +130,6 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout * we install the former (non-blocking would-block via WANT_READ) and * leave the latter NULL since we manage timeouts via PerformHandshake's * Sleep-based budget rather than mbedTLS's internal timer. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslSetBioCallCount()); @@ -159,9 +142,6 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresBioWithNonNullSendRecvAndNullRecvTimeout TEST(SolidSyslogMbedTlsStream, OpenDrivesHandshakeOnTheSslContext) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslHandshakeCallCount()); @@ -170,8 +150,6 @@ TEST(SolidSyslogMbedTlsStream, OpenDrivesHandshakeOnTheSslContext) TEST(SolidSyslogMbedTlsStream, OpenReturnsTrueWhenHandshakeSucceeds) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); MbedTlsFake_SetSslHandshakeReturn(0); CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); @@ -179,8 +157,6 @@ TEST(SolidSyslogMbedTlsStream, OpenReturnsTrueWhenHandshakeSucceeds) TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); MbedTlsFake_SetSslHandshakeReturn(-1); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); @@ -196,8 +172,6 @@ TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails) TEST(SolidSyslogMbedTlsStream, OpenRetriesHandshakeOnWantRead) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_READ); CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); @@ -206,8 +180,6 @@ TEST(SolidSyslogMbedTlsStream, OpenRetriesHandshakeOnWantRead) TEST(SolidSyslogMbedTlsStream, OpenSleepsBetweenHandshakeRetries) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_READ); SolidSyslogStream_Open(handle, addr); @@ -219,8 +191,6 @@ TEST(SolidSyslogMbedTlsStream, OpenRetriesHandshakeOnWantWrite) /* WANT_WRITE arises when mbedTLS needs to send (e.g. ClientFinished * under non-blocking transport with a temporarily-full send buffer). * Same retry treatment as WANT_READ. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_WRITE); CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); @@ -231,8 +201,6 @@ TEST(SolidSyslogMbedTlsStream, OpenFailsWhenHandshakeNeverCompletes) { /* mbedtls_ssl_handshake always returns WANT_READ — handshake never makes * progress, so the bounded budget should expire and Open returns false. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_WANT_READ); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); @@ -242,8 +210,6 @@ TEST(SolidSyslogMbedTlsStream, OpenFailsImmediatelyOnHardSslError) { /* Non-WANT error (e.g. a verify/connection failure) is fail-fast — no * retry budget burn, no Sleep. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_BAD_INPUT_DATA); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); @@ -397,8 +363,6 @@ TEST(SolidSyslogMbedTlsStream, CloseAfterInternalCloseFromSendFailureDoesNotDoub TEST(SolidSyslogMbedTlsStream, CloseSendsSslCloseNotifyOnTheSslContextFromOpen) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); SolidSyslogStream_Close(handle); @@ -409,8 +373,6 @@ TEST(SolidSyslogMbedTlsStream, CloseSendsSslCloseNotifyOnTheSslContextFromOpen) TEST(SolidSyslogMbedTlsStream, CloseFreesSslContextAndSslConfigFromOpen) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); SolidSyslogStream_Close(handle); @@ -430,8 +392,6 @@ TEST(SolidSyslogMbedTlsStream, CloseDelegatesToInjectedTransport) TEST(SolidSyslogMbedTlsStream, BioSendCallbackForwardsBufferToTransport) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); auto* bioSend = MbedTlsFake_LastSslSetBioSendCallback(); void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); @@ -447,8 +407,6 @@ TEST(SolidSyslogMbedTlsStream, BioSendCallbackForwardsBufferToTransport) TEST(SolidSyslogMbedTlsStream, BioRecvCallbackForwardsBufferToTransport) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); @@ -469,8 +427,6 @@ TEST(SolidSyslogMbedTlsStream, BioRecvReturnsWantReadWhenTransportWouldBlock) * needs MBEDTLS_ERR_SSL_WANT_READ to drive its retry loop; any other * negative is fatal. Returning -1 (or 0) here would abort the handshake * on the first non-blocking poll. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); @@ -486,8 +442,6 @@ TEST(SolidSyslogMbedTlsStream, BioRecvReturnsFatalWhenTransportFails) { /* Stream contract: negative is fatal. mbedTLS treats any negative other * than its own WANT_* sentinels as a transport error and aborts. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); SolidSyslogStream_Open(handle, addr); auto* bioRecv = MbedTlsFake_LastSslSetBioRecvCallback(); void* bioContext = MbedTlsFake_LastSslSetBioPBioArg(); @@ -502,9 +456,6 @@ TEST(SolidSyslogMbedTlsStream, BioRecvReturnsFatalWhenTransportFails) TEST(SolidSyslogMbedTlsStream, OpenSetsAuthmodeRequired) { - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslConfAuthmodeCallCount()); @@ -518,9 +469,6 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresCaChainFromConfigAndNullCrl) static mbedtls_x509_crt caChainMarker; config.CaChain = &caChainMarker; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslConfCaChainCallCount()); @@ -534,9 +482,6 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresRngFromConfigUsingCtrDrbgRandom) static mbedtls_ctr_drbg_context rngMarker; config.Rng = &rngMarker; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslConfRngCallCount()); @@ -549,9 +494,6 @@ TEST(SolidSyslogMbedTlsStream, OpenSetsHostnameWhenServerNameProvided) { config.ServerName = "syslog.example.com"; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslSetHostnameCallCount()); @@ -562,9 +504,6 @@ TEST(SolidSyslogMbedTlsStream, OpenSetsHostnameWhenServerNameProvided) TEST(SolidSyslogMbedTlsStream, OpenSkipsHostnameWhenServerNameIsNull) { /* setup() left config.ServerName at NULL. */ - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(0, MbedTlsFake_SslSetHostnameCallCount()); @@ -584,9 +523,6 @@ TEST(SolidSyslogMbedTlsStream, OpenWiresOwnCertWhenClientCertAndKeyProvided) config.ClientCertChain = &clientCertMarker; config.ClientKey = &clientKeyMarker; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(1, MbedTlsFake_SslConfOwnCertCallCount()); @@ -603,9 +539,6 @@ TEST(SolidSyslogMbedTlsStream, OpenSkipsOwnCertWhenClientCertChainIsNull) static mbedtls_pk_context clientKeyMarker; config.ClientKey = &clientKeyMarker; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(0, MbedTlsFake_SslConfOwnCertCallCount()); @@ -617,9 +550,6 @@ TEST(SolidSyslogMbedTlsStream, OpenSkipsOwnCertWhenClientKeyIsNull) static mbedtls_x509_crt clientCertMarker; config.ClientCertChain = &clientCertMarker; ReCreateHandleWithUpdatedConfig(); - SolidSyslogAddressStorage storage = {}; - struct SolidSyslogAddress* addr = SolidSyslogAddress_FromStorage(&storage); - SolidSyslogStream_Open(handle, addr); LONGS_EQUAL(0, MbedTlsFake_SslConfOwnCertCallCount()); diff --git a/Tests/MbedTlsIntegration/SocketStream.c b/Tests/MbedTlsIntegration/SocketStream.c index e199915c..3621bbc1 100644 --- a/Tests/MbedTlsIntegration/SocketStream.c +++ b/Tests/MbedTlsIntegration/SocketStream.c @@ -69,9 +69,19 @@ static bool SocketStream_Send(struct SolidSyslogStream* self, const void* buffer static SolidSyslogSsize SocketStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) { + /* Distinguish EOF (peer closed: recv == 0) from would-block (the Stream + * contract reserves 0 for "no data available, try again"). The MbedTls + * BIO recv callback maps a transport 0 to MBEDTLS_ERR_SSL_WANT_READ — + * forwarding a real EOF as 0 would loop the handshake until the budget + * exhausts. Map EOF to -1 (fatal) instead. */ struct SocketStream* stream = (struct SocketStream*) self; ssize_t n = recv(stream->Fd, buffer, size, 0); - return (SolidSyslogSsize) n; + SolidSyslogSsize result = (SolidSyslogSsize) n; + if (n == 0) + { + result = -1; + } + return result; } static void SocketStream_Close(struct SolidSyslogStream* self) diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index 49264f17..6df808a2 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -131,6 +131,22 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) leafConfig.Issuer = signingCa; MbedTlsTestCert_Create(&leafConfig, outClientCert, &rng); } + + /* Common config wiring used by every integration test: transport, sleep, + * fixture-owned DRBG, and the trusted-CA / hostname pair built in setup(). + * Per-test tweaks (e.g. swapping the CA chain to test rejection, or + * adding ClientCertChain + ClientKey for mTLS) overlay onto the returned + * struct before passing it to SolidSyslogMbedTlsStream_Create. */ + struct SolidSyslogMbedTlsStreamConfig BuildBaseConfig(struct SolidSyslogStream* transport) + { + struct SolidSyslogMbedTlsStreamConfig cfg = {}; + cfg.Transport = transport; + cfg.Sleep = NoOpSleep; + cfg.Rng = &rng; + cfg.CaChain = &trustedCa.Cert; + cfg.ServerName = kServerHostname; + return cfg; + } }; // clang-format on @@ -138,12 +154,7 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeSucceedsWhenServerCertSignedByTrustedCaAndHostnameMatches) { struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; - config.CaChain = &trustedCa.Cert; - config.ServerName = kServerHostname; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); tlsStream = SolidSyslogMbedTlsStream_Create(&config); SolidSyslogAddressStorage storage = {}; @@ -167,12 +178,8 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUn MbedTlsTestCert_Create(&untrustedConfig, &untrustedCa, &rng); struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); config.CaChain = &untrustedCa.Cert; - config.ServerName = kServerHostname; tlsStream = SolidSyslogMbedTlsStream_Create(&config); SolidSyslogAddressStorage storage = {}; @@ -186,11 +193,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUn TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMatchCert) { struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; - config.CaChain = &trustedCa.Cert; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); config.ServerName = "wrong-host.example.com"; /* server cert has SAN syslog.example.com */ tlsStream = SolidSyslogMbedTlsStream_Create(&config); @@ -213,12 +216,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeSucceedsWithClientCe CreateClientIdentitySignedBy(&clientCa, &clientCert); struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &clientCa); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; - config.CaChain = &trustedCa.Cert; - config.ServerName = kServerHostname; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); config.ClientCertChain = &clientCert.Cert; config.ClientKey = &clientCert.Key; tlsStream = SolidSyslogMbedTlsStream_Create(&config); @@ -248,12 +246,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientSe MbedTlsTestCert_Create(&clientCaConfig, &clientCa, &rng); struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &clientCa); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; - config.CaChain = &trustedCa.Cert; - config.ServerName = kServerHostname; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); tlsStream = SolidSyslogMbedTlsStream_Create(&config); SolidSyslogAddressStorage storage = {}; @@ -286,12 +279,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientCe CreateClientIdentitySignedBy(&untrustedClientCa, &clientCert); struct SolidSyslogStream* transport = StartServerRequiringClientCa(&serverCert, &trustedClientCa); - struct SolidSyslogMbedTlsStreamConfig config = {}; - config.Transport = transport; - config.Sleep = NoOpSleep; - config.Rng = &rng; - config.CaChain = &trustedCa.Cert; - config.ServerName = kServerHostname; + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); config.ClientCertChain = &clientCert.Cert; config.ClientKey = &clientCert.Key; tlsStream = SolidSyslogMbedTlsStream_Create(&config); From f7e8785b8bcfa0f6db6143a26cb41b8c9d7f7b2d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 11:57:14 +0000 Subject: [PATCH 12/13] ci: S08.07 add integration-linux-mbedtls job Parallel to integration-linux-openssl but runs under cpputest-freertos (the image carrying /opt/mbedtls + MBEDTLS_DIR, since Tests/MbedTlsIntegration/CMakeLists.txt builds mbedTLS from source via that env var). Wired into the summary job's needs list and the Quality Monitor tools table. Branch-protection rule on GitHub still needs the new "integration-linux-mbedtls" check added manually before this becomes a required status check. --- .github/workflows/ci.yml | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45faa9d0..89425c07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,49 @@ jobs: path: build/debug/cpputest_*.xml retention-days: 1 + integration-linux-mbedtls: + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + # 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 + options: --user root + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: '*' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Configure + run: cmake --preset debug + + - name: Build integration tests + run: cmake --build --preset debug --target MbedTlsIntegrationTests + + - name: Run integration tests + run: cd build/debug && ./Tests/MbedTlsIntegration/MbedTlsIntegrationTests -v -ojunit -k MbedTlsIntegrationTests + + - name: Test Report + uses: dorny/test-reporter@v3 + if: success() || failure() + with: + name: Test Results (mbedTLS Integration) + path: build/debug/cpputest_*.xml + reporter: java-junit + + - name: Upload JUnit XML + if: success() || failure() + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: junit-integration-linux-mbedtls + path: build/debug/cpputest_*.xml + retention-days: 1 + integration-windows-openssl: runs-on: windows-latest permissions: @@ -926,7 +969,7 @@ jobs: summary: if: always() && github.event_name == 'pull_request' - needs: [build-linux-gcc, build-linux-tunable-override, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-cppcheck, analyze-format, analyze-iwyu, bdd-linux-syslog-ng, build-windows-msvc, bdd-windows-otel, integration-linux-openssl, integration-windows-openssl, build-freertos-host-tdd, build-freertos-target, bdd-freertos-qemu] + needs: [build-linux-gcc, build-linux-tunable-override, build-linux-clang, sanitize-linux-gcc, coverage-linux-gcc, analyze-tidy, analyze-cppcheck, analyze-format, analyze-iwyu, bdd-linux-syslog-ng, build-windows-msvc, bdd-windows-otel, integration-linux-openssl, integration-linux-mbedtls, integration-windows-openssl, build-freertos-host-tdd, build-freertos-target, bdd-freertos-qemu] runs-on: ubuntu-latest permissions: contents: read @@ -999,6 +1042,11 @@ jobs: "name": "integration-linux-openssl", "pattern": "**/junit-integration-linux-openssl/cpputest_*.xml" }, + { + "id": "junit", + "name": "integration-linux-mbedtls", + "pattern": "**/junit-integration-linux-mbedtls/cpputest_*.xml" + }, { "id": "junit", "name": "integration-windows-openssl", From 6eb7d4897b3d5cea3d98ef1650434c42eb081121 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Thu, 21 May 2026 12:51:24 +0000 Subject: [PATCH 13/13] chore: S08.07 address CodeRabbit findings on PR #419 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three accepts from CodeRabbit's first-pass review: - Tests/MbedTlsIntegration/MbedTlsTestCert.c: extract validity strings into TEST_CERT_VALIDITY_FROM / _TO file-scope constants per the Tests/**/TEST_* convention; add an assert(written > 0) after mbedtls_x509write_crt_der so debug builds fail loudly instead of passing an empty DER to mbedtls_x509_crt_parse_der and presenting as a downstream TLS error. The existing if(written > 0) guard stays so release builds with NDEBUG remain UB-safe. - Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp: rename kServerHostname / kCaSubject / kServerSubject to TEST_SERVER_HOSTNAME / TEST_CA_SUBJECT / TEST_SERVER_SUBJECT to match the existing TEST_HOST pattern in SolidSyslogStreamSenderTest.cpp. Three remaining findings are tracked elsewhere: - CI workflow hardening (persist-credentials + dorny SHA pin) — sweep across the whole .github/workflows/ci.yml on return to main; partial fix on just the new job would have been inconsistent. - Layered-adapter Open() unwind on failure — filed as S26.02 (#420) under E26. Same shape exists in TlsStream; fix both in one go. - MbedTlsStream_Initialise / _Cleanup naming declined; the pattern mirrors TlsStream_Initialise / _Cleanup in Platform/OpenSsl and is cross-TU contract between the adapter and *Static.c, not public API. --- Tests/MbedTlsIntegration/MbedTlsTestCert.c | 14 ++++++++++++-- .../SolidSyslogMbedTlsStreamIntegrationTest.cpp | 14 +++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.c b/Tests/MbedTlsIntegration/MbedTlsTestCert.c index 36fcac97..a964eb38 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestCert.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.c @@ -1,5 +1,6 @@ #include "MbedTlsTestCert.h" +#include #include #include #include @@ -19,6 +20,12 @@ enum DER_BUFFER_BYTES = 4096 }; +/* Obviously-fake test validity window — well outside any realistic clock skew + * and far enough out that the integration tests don't need wall-clock-aware + * regeneration. Named per the TEST_* convention. */ +static const char* const TEST_CERT_VALIDITY_FROM = "20240101000000"; +static const char* const TEST_CERT_VALIDITY_TO = "20990101000000"; + /* mbedtls_x509write_crt_der writes from the END of the buffer backwards, * so the DER blob occupies [Bytes + StartOffset, Bytes + StartOffset + Length). * Capacity is the underlying storage size. Bundling these four fields keeps @@ -94,8 +101,7 @@ static void WriteCertToDer( const unsigned char serial[] = {0x01}; mbedtls_x509write_crt_set_serial_raw(&crt, (unsigned char*) serial, sizeof(serial)); - /* Long-validity test certs: 2024-01-01 to 2099-01-01. */ - mbedtls_x509write_crt_set_validity(&crt, "20240101000000", "20990101000000"); + mbedtls_x509write_crt_set_validity(&crt, TEST_CERT_VALIDITY_FROM, TEST_CERT_VALIDITY_TO); mbedtls_x509write_crt_set_basic_constraints(&crt, config->IsCa, -1); @@ -113,6 +119,10 @@ static void WriteCertToDer( /* mbedtls_x509write_crt_der writes from the END of the buffer backwards. */ int written = mbedtls_x509write_crt_der(&crt, out->Bytes, out->Capacity, mbedtls_ctr_drbg_random, rng); + /* Surface generation failures loudly in debug. The conditional store is + * retained so release builds with NDEBUG don't compute a giant size_t + * from a negative `written` and corrupt the downstream parse_der. */ + assert(written > 0); if (written > 0) { out->StartOffset = out->Capacity - (size_t) written; diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index 6df808a2..ff681f3a 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -20,9 +20,9 @@ extern "C" namespace { -constexpr const char* kServerHostname = "syslog.example.com"; -constexpr const char* kCaSubject = "CN=Test Root CA"; -constexpr const char* kServerSubject = "CN=syslog.example.com"; +constexpr const char* TEST_SERVER_HOSTNAME = "syslog.example.com"; +constexpr const char* TEST_CA_SUBJECT = "CN=Test Root CA"; +constexpr const char* TEST_SERVER_SUBJECT = "CN=syslog.example.com"; void NoOpSleep(int milliseconds) { @@ -61,13 +61,13 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) setsockopt(fds[1], SOL_SOCKET, SO_RCVTIMEO, &rcvTimeout, sizeof(rcvTimeout)); struct MbedTlsTestCertConfig caConfig = {}; - caConfig.SubjectName = kCaSubject; + caConfig.SubjectName = TEST_CA_SUBJECT; caConfig.IsCa = 1; MbedTlsTestCert_Create(&caConfig, &trustedCa, &rng); struct MbedTlsTestCertConfig serverConfig = {}; - serverConfig.SubjectName = kServerSubject; - serverConfig.SubjectAltDns = kServerHostname; + serverConfig.SubjectName = TEST_SERVER_SUBJECT; + serverConfig.SubjectAltDns = TEST_SERVER_HOSTNAME; serverConfig.IsCa = 0; serverConfig.Issuer = &trustedCa; MbedTlsTestCert_Create(&serverConfig, &serverCert, &rng); @@ -144,7 +144,7 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) cfg.Sleep = NoOpSleep; cfg.Rng = &rng; cfg.CaChain = &trustedCa.Cert; - cfg.ServerName = kServerHostname; + cfg.ServerName = TEST_SERVER_HOSTNAME; return cfg; } };