Skip to content
Merged
37 changes: 37 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13336,3 +13336,40 @@ MISRA rule — different category, doesn't set precedent.

- None outstanding. Forum post drafted for David to post under the
Libraries category at his discretion; in-repo docs land via this PR.

## 2026-05-30 — S26.04 mbedTLS client-side session resumption

### Decisions
- **Mirror the OpenSSL story's intent, independent of its code.** S26.01
(#280, OpenSSL) is still open; S26.04 is independent so we built the
mbedTLS backend first. Capture the negotiated session with
`mbedtls_ssl_get_session` after a successful handshake, feed it back via
`mbedtls_ssl_set_session` before the next handshake, on the same
`SolidSyslogMbedTlsStream` instance.
- **Free on Destroy, not Close.** The story said "Close / Destroy", but the
saved session must survive the S12.14 fail-fast Close to be resumable on
the next Open — so it lives for the life of the instance and is freed only
in `_Cleanup` (plus free-before-recapture so `get_session`'s deep copy
never leaks the prior peer cert / ticket). Eager `session_init` in Create
keeps that free always safe.
- **Best-effort, silent.** A failed capture or restore never fails the Open
and emits no error code — resumption is an optimisation, not a delivery
precondition; the sibling OpenSSL story emits nothing either. Keeps the
error channel meaningful.
- **Integration observable = server-side ticket-parse wrapper.** mbedTLS has
no client-side `SSL_session_reused()`, so the in-test server wraps its
ticket write/parse callbacks (shared `p_ticket`) and records when a
presented ticket parses successfully. A new reconnecting socketpair
transport double feeds the stream a fresh connection per Open (no TCP
ports → deterministic); a two-handshake ticket server shares one ticket
key for the resume case and rotates it for the non-resuming-peer fallback.
Verified non-vacuous by disabling the production restore and watching only
the resume assertion fail while delivery still passed.

### Deferred
- **Test/code hygiene pass.** David will take a separate PR for tidy-ups on
the new tests and adapter.
- **S26.01 (OpenSSL backend).** Still open; can now mirror this one.

### Open questions
- None outstanding.
44 changes: 44 additions & 0 deletions Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ static inline bool MbedTlsStream_BindContextToConfig(struct SolidSyslogMbedTlsSt
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 void MbedTlsStream_RestoreSession(struct SolidSyslogMbedTlsStream* self);
static inline void MbedTlsStream_CaptureSession(struct SolidSyslogMbedTlsStream* self);
static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc);
static inline bool MbedTlsStream_IsHandshakeBudgetExhausted(uint32_t totalSleptMs, uint32_t budgetMs);
static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size);
Expand Down Expand Up @@ -64,6 +66,12 @@ void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct Solid
* works without re-init. */
mbedtls_ssl_init(&self->SslContext);
mbedtls_ssl_config_init(&self->SslConfig);
/* Eager init so the session_free in Cleanup is always safe, whether or
* not a session was ever captured. The saved session outlives Close (it
* must survive the fail-fast reconnect to be resumable) and is freed only
* on Destroy. */
mbedtls_ssl_session_init(&self->SavedSession);
self->HasSavedSession = false;
}

/* Null Object substituted in Initialise when the integrator does not install a
Expand Down Expand Up @@ -96,9 +104,15 @@ static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct

void MbedTlsStream_Cleanup(struct SolidSyslogStream* base)
{
struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base);
/* Mirror the OpenSSL TlsStream pattern: an integrator who destroys a
* still-Open stream must not leak the underlying TLS state. */
MbedTlsStream_Close(base);
/* Release the saved resumption session — Destroy is the one site that
* frees it, since it must survive every Close to stay resumable. Safe on
* an empty session thanks to the eager init in Initialise. */
mbedtls_ssl_session_free(&self->SavedSession);
self->HasSavedSession = false;
/* 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();
Expand Down Expand Up @@ -129,15 +143,45 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru
if (ok)
{
MbedTlsStream_InstallTransportCallbacks(self);
MbedTlsStream_RestoreSession(self);
ok = MbedTlsStream_PerformHandshake(self);
}
if (ok)
{
MbedTlsStream_CaptureSession(self);
}
if (!ok)
{
MbedTlsStream_Close(base);
}
return ok;
}

/* Feed a previously-captured session back so this handshake can resume it.
* No-op on the first connect (nothing saved yet). */
static inline void MbedTlsStream_RestoreSession(struct SolidSyslogMbedTlsStream* self)
{
if (self->HasSavedSession)
{
(void) mbedtls_ssl_set_session(&self->SslContext, &self->SavedSession);
}
}

/* Save the just-negotiated session so the next Open of this instance can
* resume it. Best-effort: a non-zero return leaves HasSavedSession false so
* the next connect simply does a full handshake. */
static inline void MbedTlsStream_CaptureSession(struct SolidSyslogMbedTlsStream* self)
{
/* get_session deep-copies into SavedSession; free any prior session first
* so its peer cert / ticket allocations are not leaked. */
if (self->HasSavedSession)
{
mbedtls_ssl_session_free(&self->SavedSession);
self->HasSavedSession = false;
}
self->HasSavedSession = mbedtls_ssl_get_session(&self->SslContext, &self->SavedSession) == 0;
}

static inline bool MbedTlsStream_ApplySslConfigDefaults(struct SolidSyslogMbedTlsStream* self)
{
bool ok = mbedtls_ssl_config_defaults(
Expand Down
3 changes: 3 additions & 0 deletions Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H

#include <mbedtls/ssl.h>
#include <stdbool.h>

#include "SolidSyslogMbedTlsStream.h"
#include "SolidSyslogStreamDefinition.h"
Expand All @@ -12,6 +13,8 @@ struct SolidSyslogMbedTlsStream
struct SolidSyslogMbedTlsStreamConfig Config;
mbedtls_ssl_config SslConfig;
mbedtls_ssl_context SslContext;
mbedtls_ssl_session SavedSession; /* resumption: captured after handshake, fed back on next Open */
bool HasSavedSession;
};

void MbedTlsStream_Initialise(struct SolidSyslogStream* base, const struct SolidSyslogMbedTlsStreamConfig* config);
Expand Down
147 changes: 147 additions & 0 deletions Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ TEST(SolidSyslogMbedTlsStream, CreateInitialisesSslConfigForSafeFree)
LONGS_EQUAL(1, MbedTlsFake_SslConfigInitCallCount());
}

TEST(SolidSyslogMbedTlsStream, CreateInitialisesSavedSessionForSafeFree)

{
/* The per-instance saved-session slot (session resumption, S26.04) is
* init'd eagerly in Create so the session_free in Destroy is always safe
* — whether a session was ever captured or not. Same eager-init invariant
* as the SslConfig / SslContext cases. */
LONGS_EQUAL(1, MbedTlsFake_SslSessionInitCallCount());
}

TEST(SolidSyslogMbedTlsStream, OpenAppliesClientStreamDefaultsToSslConfig)

{
Expand Down Expand Up @@ -230,6 +240,143 @@ TEST(SolidSyslogMbedTlsStream, OpenReturnsFalseWhenHandshakeFails)
CHECK_FALSE(SolidSyslogStream_Open(handle, addr));
}

/* -------------------------------------------------------------------------
* Session resumption (S26.04). After a successful handshake the adapter
* captures the negotiated session into its per-instance SavedSession slot,
* feeds it back via mbedtls_ssl_set_session before the next handshake, and
* frees it on Destroy (not Close — it must survive the fail-fast reconnect).
* Best-effort: any failure falls back to a full handshake; delivery never
* depends on resumption.
* ------------------------------------------------------------------------- */

TEST(SolidSyslogMbedTlsStream, OpenCapturesSessionWhenHandshakeSucceeds)

{
MbedTlsFake_SetSslHandshakeReturn(0);

SolidSyslogStream_Open(handle, addr);

LONGS_EQUAL(1, MbedTlsFake_SslGetSessionCallCount());
POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslGetSessionContextArg());
POINTERS_EQUAL(MbedTlsFake_LastSslSessionInitArg(), MbedTlsFake_LastSslGetSessionSessionArg());
}

TEST(SolidSyslogMbedTlsStream, OpenDoesNotCaptureSessionWhenHandshakeFails)

{
/* A failed handshake leaves no session to save — capture must be gated on
* handshake success so we never resume from a half-built session. */
MbedTlsFake_SetSslHandshakeReturn(-1);

SolidSyslogStream_Open(handle, addr);

LONGS_EQUAL(0, MbedTlsFake_SslGetSessionCallCount());
}

TEST(SolidSyslogMbedTlsStream, OpenSucceedsEvenWhenSessionCaptureFails)

{
/* Resumption is best-effort: a get_session failure must not fail the Open
* — the handshake completed, the message can be delivered, the next
* connect simply won't resume. */
MbedTlsFake_SetSslHandshakeReturn(0);
MbedTlsFake_SetSslGetSessionReturn(-1);

CHECK_TRUE(SolidSyslogStream_Open(handle, addr));
}

TEST(SolidSyslogMbedTlsStream, FirstOpenDoesNotRestoreSession)

{
/* Nothing captured yet on a fresh stream, so the first handshake must run
* full — no set_session call. */
MbedTlsFake_SetSslHandshakeReturn(0);

SolidSyslogStream_Open(handle, addr);

LONGS_EQUAL(0, MbedTlsFake_SslSetSessionCallCount());
}

TEST(SolidSyslogMbedTlsStream, SecondOpenRestoresSavedSessionBeforeHandshake)

{
/* First Open captures; the reconnect Close tears the connection down; the
* second Open of the same instance feeds the saved session back via
* set_session *before* the handshake it primes. */
MbedTlsFake_SetSslHandshakeReturn(0);
SolidSyslogStream_Open(handle, addr);
SolidSyslogStream_Close(handle);

SolidSyslogStream_Open(handle, addr);

LONGS_EQUAL(1, MbedTlsFake_SslSetSessionCallCount());
POINTERS_EQUAL(MbedTlsFake_LastSslInitArg(), MbedTlsFake_LastSslSetSessionContextArg());
POINTERS_EQUAL(MbedTlsFake_LastSslSessionInitArg(), MbedTlsFake_LastSslSetSessionSessionArg());
/* One handshake (the first Open's) had completed when set_session ran,
* proving the restore precedes the second Open's handshake. */
LONGS_EQUAL(1, MbedTlsFake_SslSetSessionHandshakeCountAtCall());
}

TEST(SolidSyslogMbedTlsStream, SecondOpenSucceedsWhenSessionRestoreFails)

{
/* Best-effort restore: a set_session failure must not fail the Open — the
* adapter falls through to a full handshake and still delivers. */
MbedTlsFake_SetSslHandshakeReturn(0);
SolidSyslogStream_Open(handle, addr);
SolidSyslogStream_Close(handle);
MbedTlsFake_SetSslSetSessionReturn(-1);

CHECK_TRUE(SolidSyslogStream_Open(handle, addr));
}

TEST(SolidSyslogMbedTlsStream, CloseDoesNotFreeSavedSession)

{
/* The lifecycle lock-in: Close runs on every fail-fast reconnect, so it
* must NOT free the saved session — otherwise the very next Open could
* never resume. The session is freed only on Destroy. */
MbedTlsFake_SetSslHandshakeReturn(0);
SolidSyslogStream_Open(handle, addr);

SolidSyslogStream_Close(handle);

LONGS_EQUAL(0, MbedTlsFake_SslSessionFreeCallCount());
}

TEST(SolidSyslogMbedTlsStream, DestroyFreesSavedSession)

{
/* Destroy is the one site that releases the saved session — symmetric with
* the eager session_init in Create. */
MbedTlsFake_SetSslHandshakeReturn(0);
SolidSyslogStream_Open(handle, addr);

SolidSyslogMbedTlsStream_Destroy(handle);
LONGS_EQUAL(1, MbedTlsFake_SslSessionFreeCallCount());
POINTERS_EQUAL(MbedTlsFake_LastSslSessionInitArg(), MbedTlsFake_LastSslSessionFreeArg());

handle = SolidSyslogMbedTlsStream_Create(&config); /* keep teardown's Destroy valid */
}

TEST(SolidSyslogMbedTlsStream, SecondCaptureFreesPreviousSession)

{
/* mbedtls_ssl_get_session deep-copies into the destination; capturing a
* second session over a populated slot would leak the first's peer cert /
* ticket. The recapture must free the prior session first. Close in
* between does not free (lifecycle), so exactly one free is observed. */
MbedTlsFake_SetSslHandshakeReturn(0);
SolidSyslogStream_Open(handle, addr); /* capture #1 */
SolidSyslogStream_Close(handle); /* survives — no free */

SolidSyslogStream_Open(handle, addr); /* recapture #2 frees #1 first */

LONGS_EQUAL(1, MbedTlsFake_SslSessionFreeCallCount());
LONGS_EQUAL(2, MbedTlsFake_SslGetSessionCallCount());
POINTERS_EQUAL(MbedTlsFake_LastSslSessionInitArg(), MbedTlsFake_LastSslSessionFreeArg());
}

/* -------------------------------------------------------------------------
* Bounded handshake retry loop. mbedtls_ssl_handshake under non-blocking
* transport will emit MBEDTLS_ERR_SSL_WANT_READ / WANT_WRITE between RTTs;
Expand Down
2 changes: 2 additions & 0 deletions Tests/MbedTlsIntegration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ add_executable(MbedTlsIntegrationTests
main.cpp
SolidSyslogMbedTlsStreamIntegrationTest.cpp
SocketStream.c
ReconnectingSocketStream.c
MbedTlsTestCert.c
MbedTlsTestServer.c
MbedTlsResumptionServer.c
${CMAKE_SOURCE_DIR}/Tests/AddressFake.c
${CMAKE_SOURCE_DIR}/Tests/Support/SafeStringStandard.c
${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c
Expand Down
Loading
Loading