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", 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/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..dce6e728 --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -0,0 +1,256 @@ +#include "SolidSyslogMbedTlsStream.h" + +#include +#include +#include +#include + +#include "SolidSyslogMbedTlsStreamPrivate.h" +#include "SolidSyslogNullStream.h" +#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_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); +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) +{ + struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); + self->Base.Open = MbedTlsStream_Open; + self->Base.Send = MbedTlsStream_Send; + self->Base.Read = MbedTlsStream_Read; + self->Base.Close = MbedTlsStream_Close; + self->Config = *config; + /* 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. */ + 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(); +} + +static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct SolidSyslogStream* base) +{ + 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) && MbedTlsStream_ApplySslConfigDefaults(self); + if (ok) + { + MbedTlsStream_ApplyTlsPolicy(self); + ok = MbedTlsStream_BindContextToConfig(self) && MbedTlsStream_ConfigureExpectedHostname(self); + } + if (ok) + { + MbedTlsStream_InstallTransportCallbacks(self); + ok = MbedTlsStream_PerformHandshake(self); + } + 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)) + { + (void) mbedtls_ssl_conf_own_cert(&self->SslConfig, self->Config.ClientCertChain, self->Config.ClientKey); + } +} + +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; + } + 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 + * 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; + 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; + SolidSyslogSsize n = SolidSyslogStream_Read(self->Config.Transport, buf, len); + int result = -1; + if (n > 0) + { + result = (int) n; + } + else if (n == 0) + { + result = MBEDTLS_ERR_SSL_WANT_READ; + } + 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); + 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); + 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; + } + 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); + (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 new file mode 100644 index 00000000..e6661a89 --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -0,0 +1,20 @@ +#ifndef SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H +#define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H + +#include + +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStreamDefinition.h" + +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); +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..9917155a 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,16 @@ foreach(fatfs_test_target IN ITEMS endif() endforeach() +foreach(mbedtls_test_target IN ITEMS + SolidSyslogMbedTlsStreamPoolTest + SolidSyslogMbedTlsStreamTest + 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..2ace48d8 --- /dev/null +++ b/Tests/MbedTls/CMakeLists.txt @@ -0,0 +1,78 @@ +# 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 + ${CMAKE_SOURCE_DIR}/Tests/StreamFake.c +) + +target_link_libraries(SolidSyslogMbedTlsStreamPoolTest PRIVATE + ${PROJECT_NAME} + ConfigLockFake + ErrorHandlerFake + MbedTlsFakes + CppUTest + CppUTestExt +) + +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` +# 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/SolidSyslogMbedTlsStreamPoolTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp new file mode 100644 index 00000000..72c7039c --- /dev/null +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamPoolTest.cpp @@ -0,0 +1,192 @@ +#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 "StreamFake.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 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) + { + 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); + } + StreamFake_Destroy(transport); + 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/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp new file mode 100644 index 00000000..7eb61efc --- /dev/null +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -0,0 +1,556 @@ +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include +#include + +#include "MbedTlsFake.h" +#include "SolidSyslogAddress.h" +#include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" +#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 + +static int NoOpSleepCallCount; +static int g_lastSleepMs; + +static void NoOpSleep(int milliseconds) +{ + NoOpSleepCallCount++; + g_lastSleepMs = milliseconds; +} + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsStream) +{ + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogStream* handle = nullptr; + struct SolidSyslogMbedTlsStreamConfig config = {}; + SolidSyslogAddressStorage addrStorage = {}; + struct SolidSyslogAddress* addr = nullptr; + + void setup() override + { + MbedTlsFake_Reset(); + NoOpSleepCallCount = 0; + g_lastSleepMs = 0; + transport = StreamFake_Create(); + 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 + { + 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); + } + + /* 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 + +TEST(SolidSyslogMbedTlsStream, OpenDelegatesToInjectedTransport) +{ + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, StreamFake_OpenCallCount(transport)); + POINTERS_EQUAL(addr, StreamFake_LastOpenAddr(transport)); +} + +TEST(SolidSyslogMbedTlsStream, CreateInitialisesSslConfigForSafeFree) +{ + /* 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) +{ + 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, CreateInitialisesSslContextForSafeFree) +{ + /* Same eager-init invariant as the SslConfig case above. */ + LONGS_EQUAL(1, MbedTlsFake_SslInitCallCount()); +} + +TEST(SolidSyslogMbedTlsStream, OpenBindsContextToConfig) +{ + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslSetupCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslSetupContextArg()); + POINTERS_EQUAL(MbedTlsFake_LastSslConfigInitArg(), MbedTlsFake_LastSslSetupConfigArg()); +} + +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. */ + 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); + POINTERS_EQUAL(nullptr, (void*) MbedTlsFake_LastSslSetBioRecvTimeoutCallback()); +} + +TEST(SolidSyslogMbedTlsStream, OpenDrivesHandshakeOnTheSslContext) +{ + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(1, MbedTlsFake_SslHandshakeCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslHandshakeArg()); +} + +TEST(SolidSyslogMbedTlsStream, OpenReturnsTrueWhenHandshakeSucceeds) +{ + MbedTlsFake_SetSslHandshakeReturn(0); + + CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); +} + +TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails) +{ + MbedTlsFake_SetSslHandshakeReturn(-1); + + 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) +{ + ArrangeHandshakeRetryThenSucceed(MBEDTLS_ERR_SSL_WANT_READ); + + CHECK_TRUE(SolidSyslogStream_Open(handle, addr)); + CALLED_FAKE(MbedTlsFake_SslHandshake, TWICE); +} + +TEST(SolidSyslogMbedTlsStream, OpenSleepsBetweenHandshakeRetries) +{ + 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. */ + 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. */ + 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. */ + 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}; + + 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, 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]; + 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, 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) +{ + SolidSyslogStream_Open(handle, addr); + + SolidSyslogStream_Close(handle); + + LONGS_EQUAL(1, MbedTlsFake_SslCloseNotifyCallCount()); + POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslCloseNotifyArg()); +} + +TEST(SolidSyslogMbedTlsStream, CloseFreesSslContextAndSslConfigFromOpen) +{ + 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) +{ + 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) +{ + 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)); +} + +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. */ + 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. */ + 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) +{ + 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(); + 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(); + 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(); + 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. */ + SolidSyslogStream_Open(handle, addr); + + 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(); + 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(); + 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(); + SolidSyslogStream_Open(handle, addr); + + LONGS_EQUAL(0, MbedTlsFake_SslConfOwnCertCallCount()); +} 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..584be079 --- /dev/null +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -0,0 +1,86 @@ +# 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 + 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 +) + +target_link_libraries(MbedTlsIntegrationTests PRIVATE + ${PROJECT_NAME} + CppUTest + CppUTestExt + 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_SOURCE_DIR}/Tests/Support + ${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/MbedTlsTestCert.c b/Tests/MbedTlsIntegration/MbedTlsTestCert.c new file mode 100644 index 00000000..a964eb38 --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.c @@ -0,0 +1,133 @@ +#include "MbedTlsTestCert.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SafeString.h" + +enum +{ + RSA_KEY_BITS = 2048, + RSA_EXPONENT = 65537, + 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 + * 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, + mbedtls_ctr_drbg_context* rng, + struct DerBuffer* out +); + +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); + SafeString_Copy(out->SubjectName, sizeof(out->SubjectName), config->SubjectName); + + GenerateKey(&out->Key, 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, &der.Bytes[der.StartOffset], der.Length); +} + +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, + mbedtls_ctr_drbg_context* rng, + struct DerBuffer* out +) +{ + 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)); + + mbedtls_x509write_crt_set_validity(&crt, TEST_CERT_VALIDITY_FROM, TEST_CERT_VALIDITY_TO); + + 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 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; + out->Length = (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..484560ca --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.c @@ -0,0 +1,145 @@ +#include "MbedTlsTestServer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MbedTlsTestCert.h" + +struct MbedTlsTestServer +{ + int Fd; + mbedtls_ssl_config SslConfig; + mbedtls_ssl_context SslContext; + pthread_t Thread; + bool ThreadJoined; + bool HandshakeSucceeded; +}; + +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 + ); + /* 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); + 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, + (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) + { + 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; +} + +/* 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; + + 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); + 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..185357e6 --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsTestServer.h @@ -0,0 +1,38 @@ +#ifndef MBEDTLSTESTSERVER_H +#define MBEDTLSTESTSERVER_H + +#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 */ + /* 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. 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); + +EXTERN_C_END + +#endif /* MBEDTLSTESTSERVER_H */ diff --git a/Tests/MbedTlsIntegration/SocketStream.c b/Tests/MbedTlsIntegration/SocketStream.c new file mode 100644 index 00000000..3621bbc1 --- /dev/null +++ b/Tests/MbedTlsIntegration/SocketStream.c @@ -0,0 +1,95 @@ +#include "SocketStream.h" + +#include +#include +#include +#include + +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" + +struct SolidSyslogAddress; + +struct SocketStream +{ + struct SolidSyslogStream Base; + int Fd; +}; + +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 = 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) +{ + /* 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 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. */ + (void) self; + (void) addr; + return true; +} + +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; + 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 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); + SolidSyslogSsize result = (SolidSyslogSsize) n; + if (n == 0) + { + result = -1; + } + return result; +} + +static void SocketStream_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 new file mode 100644 index 00000000..ff681f3a --- /dev/null +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -0,0 +1,305 @@ +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include +#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* 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) +{ + (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); + /* 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 = TEST_CA_SUBJECT; + caConfig.IsCa = 1; + MbedTlsTestCert_Create(&caConfig, &trustedCa, &rng); + + struct MbedTlsTestCertConfig serverConfig = {}; + serverConfig.SubjectName = TEST_SERVER_SUBJECT; + serverConfig.SubjectAltDns = TEST_SERVER_HOSTNAME; + 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) + { + return StartServerRequiringClientCa(cert, nullptr); + } + + /* 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]; + 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); + } + + /* 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 = TEST_SERVER_HOSTNAME; + return cfg; + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeSucceedsWhenServerCertSignedByTrustedCaAndHostnameMatches) +{ + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + 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" + ); +} + +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 = BuildBaseConfig(transport); + config.CaChain = &untrustedCa.Cert; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + SolidSyslogAddressStorage storage = {}; + bool opened = SolidSyslogStream_Open(tlsStream, SolidSyslogAddress_FromStorage(&storage)); + + CHECK_FALSE_TEXT(opened, "client-side handshake must fail when the server cert chains to an untrusted CA"); + + MbedTlsTestCert_Destroy(&untrustedCa); +} + +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMatchCert) +{ + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + 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, 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 = BuildBaseConfig(transport); + 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 = BuildBaseConfig(transport); + 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 = BuildBaseConfig(transport); + 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 + * 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..daba081f --- /dev/null +++ b/Tests/MbedTlsIntegration/main.cpp @@ -0,0 +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); +} 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..d30455cb --- /dev/null +++ b/Tests/Support/MbedTlsFake.c @@ -0,0 +1,614 @@ +#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 */ +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; +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; + +/* 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 */ +static int sslConfRngCallCount; +static mbedtls_ssl_config* lastSslConfRngConfigArg; +static int (*lastSslConfRngFuncArg)(void*, unsigned char*, size_t); +static void* lastSslConfRngContextArg; + +/* mbedtls_ssl_set_hostname */ +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. + * ------------------------------------------------------------------------- */ + +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; + sslHandshakeReturnSequenceLen = 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; + 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; + sslConfOwnCertCallCount = 0; + lastSslConfOwnCertConfigArg = NULL; + lastSslConfOwnCertCertArg = NULL; + lastSslConfOwnCertKeyArg = 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; +} + +mbedtls_ssl_recv_timeout_t* MbedTlsFake_LastSslSetBioRecvTimeoutCallback(void) +{ + return lastSslSetBioRecvTimeoutCallback; +} + +int MbedTlsFake_SslHandshakeCallCount(void) +{ + return sslHandshakeCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslHandshakeArg(void) +{ + return lastSslHandshakeArg; +} + +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; +} + +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; +} + +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; +} + +int (*MbedTlsFake_LastSslConfRngFuncArg(void))(void*, unsigned char*, size_t) +{ + 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; +} + +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. + * ------------------------------------------------------------------------- */ + +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) +{ + int callIndex = sslHandshakeCallCount; + sslHandshakeCallCount++; + lastSslHandshakeArg = ssl; + 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) +{ + 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; +} + +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; +} + +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, int (*f_rng)(void*, unsigned char*, size_t), 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. */ +// 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; + (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 new file mode 100644 index 00000000..84d8dc5e --- /dev/null +++ b/Tests/Support/MbedTlsFake.h @@ -0,0 +1,115 @@ +#ifndef MBEDTLSFAKE_H +#define MBEDTLSFAKE_H + +#include +#include + +#include "ExternC.h" + +struct mbedtls_ssl_config; +struct mbedtls_ssl_context; +struct mbedtls_x509_crt; +struct mbedtls_x509_crl; +struct mbedtls_ctr_drbg_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); + int (*MbedTlsFake_LastSslSetBioRecvTimeoutCallback(void))(void*, unsigned char*, size_t, uint32_t); + + /* mbedtls_ssl_handshake */ + 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); + 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); + + /* 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); + + /* 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 */