Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 50 additions & 37 deletions DEVLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,55 @@
# Dev Log

## 2026-05-31 — Drop TLS session resumption from both reference integrations (revert S26.04, won't-do S26.01)

Decision session, not a feature. TLS session resumption is now **out of scope**
for both reference TLS integrations (`Platform/OpenSsl/`, `Platform/MbedTls/`),
on either backend, for either TLS 1.2 or TLS 1.3. This commit reverts the merged
mbedTLS work (S26.04, #489) and the OpenSSL story (S26.01, #280) is closed
won't-do.

### Decision

- **mbedTLS (S26.04, #489, already on main):** reverted here via a clean
single-parent `git revert d37ef31`. The production diff was purely additive
(42 add / 0 del) with no bundled bug fix to preserve, so the revert is total.
- **OpenSSL (S26.01, #280):** won't-do. The in-progress slices 1–5 are dropped
(abandoned `feat/s26-01-openssl-session-resumption` branch). The one genuine
independent bug found along the way — `SSL_CTX` leaked on `Close` under the
fail-fast reconnect cycle — is kept and lands as its own PR
(`fix/openssl-tlsstream-ctx-leak`).

### Why (architectural rationale)

1. **Resumption is an optimization, never correctness.** A full handshake must
always succeed on every (re)connect without disrupting core operation;
products must be architected for that regardless.
2. **Long-lived connections make it near-worthless here.** The TCP streams use
keepalive — a syslog client does one handshake then streams for a long time.
Resumption only pays at reconnect, and reconnects are rare.
3. **When reconnects DO happen, resumption is least likely to be available.** A
reconnect is usually triggered by a disgraceful close — exactly when the
server has discarded the state needed to resume. The optimization tends to be
absent in the very case it was meant to help.
4. **Disproportionate cost/complexity to do honestly.** TLS 1.2 (RFC 5077) and
1.3 (RFC 8446 PSK) use different mechanisms; 1.3 tickets arrive post-handshake
and need a defined read to capture; mTLS-on-resume has separate identity
semantics. Four real cases ({1.2,1.3} × {server-auth, mTLS}), each needing an
honest mechanism-correct test — a large surface for an optional speedup.

TLS 1.3 0-RTT / early data was already out of scope and stays out.

### Test-strategy lesson (if ever revisited)

Don't hand-roll a TLS server in the harness — it encodes our own fallible
understanding of resumption semantics, so a failing test can't distinguish a
client bug from a server-config bug. This cost real time. Prefer driving the
**real sender stack** against an **off-the-shelf** server (`openssl s_server` /
stunnel), observed **server-side** — one backend-neutral server covers both
OpenSSL and mbedTLS clients and all four cases via flags. That is process-level
(real port), so it belongs in the BDD/compose layer, not the in-process CppUTest
binary.

## 2026-05-30 — S28.08 SolidSyslogLwipRawDnsResolver (DNS via dns_gethostbyname) + FreeRtosLwip BDD by-name

Last implementation story of E28. Adds the DNS sibling of the numeric
Expand Down Expand Up @@ -13336,40 +13386,3 @@ 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: 0 additions & 44 deletions Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ 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 @@ -66,12 +64,6 @@ 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 @@ -104,15 +96,9 @@ 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 @@ -143,45 +129,15 @@ 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: 0 additions & 3 deletions Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H

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

#include "SolidSyslogMbedTlsStream.h"
#include "SolidSyslogStreamDefinition.h"
Expand All @@ -13,8 +12,6 @@ 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: 0 additions & 147 deletions Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,6 @@ 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 @@ -240,143 +230,6 @@ 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: 0 additions & 2 deletions Tests/MbedTlsIntegration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ 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