diff --git a/DEVLOG.md b/DEVLOG.md index a84a0e98..a0c0c3cb 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -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. diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index fcc308cb..63a90b6a 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -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); @@ -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 @@ -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(); @@ -129,8 +143,13 @@ 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); @@ -138,6 +157,31 @@ static inline bool MbedTlsStream_Open(struct SolidSyslogStream* base, const stru 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( diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h index e6661a89..4a76ef09 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamPrivate.h @@ -2,6 +2,7 @@ #define SOLIDSYSLOGMBEDTLSSTREAMPRIVATE_H #include +#include #include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogStreamDefinition.h" @@ -12,6 +13,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); diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 8ce71acb..13aaafbf 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -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) { @@ -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; diff --git a/Tests/MbedTlsIntegration/CMakeLists.txt b/Tests/MbedTlsIntegration/CMakeLists.txt index a8c554da..141fa385 100644 --- a/Tests/MbedTlsIntegration/CMakeLists.txt +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -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 diff --git a/Tests/MbedTlsIntegration/MbedTlsResumptionServer.c b/Tests/MbedTlsIntegration/MbedTlsResumptionServer.c new file mode 100644 index 00000000..4caff602 --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsResumptionServer.c @@ -0,0 +1,261 @@ +#include "MbedTlsResumptionServer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MbedTlsTestCert.h" +#include "mbedtls/pk.h" + +enum +{ + CONNECTION_COUNT = 2, + TICKET_LIFETIME_SECONDS = 86400, + APP_RECORD_BUFFER_BYTES = 512 +}; + +/* Shared by the wrapped ticket write / parse callbacks. `Ctx` is the real + * ticket context the wrappers forward to; swapping it before the second + * connection is how the rotate-key (non-resuming-peer) case is staged. + * `Accepted` is set true when a presented ticket parses successfully — the + * stand-in for mbedTLS's missing client-side SSL_session_reused(). */ +struct TicketCallbackContext +{ + mbedtls_ssl_ticket_context* Ctx; + bool Accepted; +}; + +struct MbedTlsResumptionServer +{ + int Fds[CONNECTION_COUNT]; + bool RotateKey; + mbedtls_ctr_drbg_context* Rng; + const struct MbedTlsTestCert* ServerCert; + mbedtls_ssl_config Conf; + mbedtls_ssl_ticket_context TicketCtxPrimary; + mbedtls_ssl_ticket_context TicketCtxRotated; + struct TicketCallbackContext TicketCb; + pthread_t Thread; + bool Joined; + bool HandshakeSucceeded[CONNECTION_COUNT]; + bool MessageDelivered[CONNECTION_COUNT]; + bool ResumeAccepted[CONNECTION_COUNT]; +}; + +static void* RunServer(void* arg); +static int WrappedTicketWrite( + void* p_ticket, + const mbedtls_ssl_session* session, + unsigned char* start, + const unsigned char* end, + size_t* tlen, + uint32_t* lifetime +); +static int WrappedTicketParse(void* p_ticket, mbedtls_ssl_session* session, unsigned char* buf, size_t len); +static int ServerBioSend(void* ctx, const unsigned char* buf, size_t len); +static int ServerBioRecv(void* ctx, unsigned char* buf, size_t len); + +struct MbedTlsResumptionServer* MbedTlsResumptionServer_Create(const struct MbedTlsResumptionServerConfig* config) +{ + struct MbedTlsResumptionServer* self = + (struct MbedTlsResumptionServer*) calloc(1U, sizeof(struct MbedTlsResumptionServer)); + self->Fds[0] = config->ServerFds[0]; + self->Fds[1] = config->ServerFds[1]; + self->RotateKey = config->RotateTicketKeyBetweenConnections; + self->Rng = config->Rng; + self->ServerCert = config->ServerCert; + + mbedtls_ssl_config_init(&self->Conf); + mbedtls_ssl_config_defaults( + &self->Conf, + MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ); + /* Pin TLS 1.2 — same rationale as MbedTlsTestServer: keeps the rejection / + * teardown cadence synchronous over a socketpair, and the TLS 1.2 ticket + * arrives in-handshake so the client can capture it immediately. */ + mbedtls_ssl_conf_max_tls_version(&self->Conf, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_rng(&self->Conf, mbedtls_ctr_drbg_random, self->Rng); + /* Server-auth only — these tests exercise resumption, not client identity. */ + mbedtls_ssl_conf_authmode(&self->Conf, MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_own_cert( + &self->Conf, + (mbedtls_x509_crt*) &self->ServerCert->Cert, + (mbedtls_pk_context*) &self->ServerCert->Key + ); + + mbedtls_ssl_ticket_init(&self->TicketCtxPrimary); + mbedtls_ssl_ticket_setup( + &self->TicketCtxPrimary, + mbedtls_ctr_drbg_random, + self->Rng, + MBEDTLS_CIPHER_AES_256_GCM, + TICKET_LIFETIME_SECONDS + ); + mbedtls_ssl_ticket_init(&self->TicketCtxRotated); /* setup lazily in RunServer if rotating */ + self->TicketCb.Ctx = &self->TicketCtxPrimary; + self->TicketCb.Accepted = false; + mbedtls_ssl_conf_session_tickets_cb(&self->Conf, WrappedTicketWrite, WrappedTicketParse, &self->TicketCb); + + pthread_create(&self->Thread, NULL, RunServer, self); + return self; +} + +void MbedTlsResumptionServer_Destroy(struct MbedTlsResumptionServer* self) +{ + if (self != NULL) + { + if (!self->Joined) + { + /* Unblock a worker that may be parked in recv on a wedged + * negative-path test, then join. */ + for (int i = 0; i < CONNECTION_COUNT; i++) + { + if (self->Fds[i] >= 0) + { + shutdown(self->Fds[i], SHUT_RDWR); + } + } + pthread_join(self->Thread, NULL); + self->Joined = true; + } + mbedtls_ssl_config_free(&self->Conf); + mbedtls_ssl_ticket_free(&self->TicketCtxPrimary); + mbedtls_ssl_ticket_free(&self->TicketCtxRotated); + for (int i = 0; i < CONNECTION_COUNT; i++) + { + if (self->Fds[i] >= 0) + { + close(self->Fds[i]); + } + } + free(self); + } +} + +void MbedTlsResumptionServer_Join(struct MbedTlsResumptionServer* self) +{ + if (!self->Joined) + { + pthread_join(self->Thread, NULL); + self->Joined = true; + } +} + +bool MbedTlsResumptionServer_BothHandshakesSucceeded(struct MbedTlsResumptionServer* self) +{ + return self->HandshakeSucceeded[0] && self->HandshakeSucceeded[1]; +} + +bool MbedTlsResumptionServer_BothMessagesDelivered(struct MbedTlsResumptionServer* self) +{ + return self->MessageDelivered[0] && self->MessageDelivered[1]; +} + +bool MbedTlsResumptionServer_SecondHandshakeResumed(struct MbedTlsResumptionServer* self) +{ + return self->ResumeAccepted[1]; +} + +/* Drives two consecutive server-side handshakes — one per fd — reading a + * single application record after each so the test can assert delivery on + * both the full and the resumed connection. */ +static void* RunServer(void* arg) +{ + struct MbedTlsResumptionServer* self = (struct MbedTlsResumptionServer*) arg; + + for (int i = 0; i < CONNECTION_COUNT; i++) + { + if ((i == 1) && self->RotateKey) + { + /* Fresh key the client's saved ticket cannot decrypt -> the second + * handshake falls back to full. */ + mbedtls_ssl_ticket_setup( + &self->TicketCtxRotated, + mbedtls_ctr_drbg_random, + self->Rng, + MBEDTLS_CIPHER_AES_256_GCM, + TICKET_LIFETIME_SECONDS + ); + self->TicketCb.Ctx = &self->TicketCtxRotated; + } + self->TicketCb.Accepted = false; + + mbedtls_ssl_context ssl; + mbedtls_ssl_init(&ssl); + mbedtls_ssl_setup(&ssl, &self->Conf); + mbedtls_ssl_set_bio(&ssl, &self->Fds[i], ServerBioSend, ServerBioRecv, NULL); + + int handshakeRc = 0; + do + { + handshakeRc = mbedtls_ssl_handshake(&ssl); + } while ((handshakeRc == MBEDTLS_ERR_SSL_WANT_READ) || (handshakeRc == MBEDTLS_ERR_SSL_WANT_WRITE)); + + self->HandshakeSucceeded[i] = (handshakeRc == 0); + self->ResumeAccepted[i] = self->TicketCb.Accepted; + + if (handshakeRc == 0) + { + unsigned char buffer[APP_RECORD_BUFFER_BYTES]; + int readRc = 0; + do + { + readRc = mbedtls_ssl_read(&ssl, buffer, sizeof(buffer)); + } while ((readRc == MBEDTLS_ERR_SSL_WANT_READ) || (readRc == MBEDTLS_ERR_SSL_WANT_WRITE)); + self->MessageDelivered[i] = (readRc > 0); + } + + mbedtls_ssl_free(&ssl); + } + return NULL; +} + +static int WrappedTicketWrite( + void* p_ticket, + const mbedtls_ssl_session* session, + unsigned char* start, + const unsigned char* end, + size_t* tlen, + uint32_t* lifetime +) +{ + struct TicketCallbackContext* cb = (struct TicketCallbackContext*) p_ticket; + return mbedtls_ssl_ticket_write(cb->Ctx, session, start, end, tlen, lifetime); +} + +static int WrappedTicketParse(void* p_ticket, mbedtls_ssl_session* session, unsigned char* buf, size_t len) +{ + struct TicketCallbackContext* cb = (struct TicketCallbackContext*) p_ticket; + int rc = mbedtls_ssl_ticket_parse(cb->Ctx, session, buf, len); + if (rc == 0) + { + cb->Accepted = true; /* a valid ticket was presented -> this handshake resumed */ + } + return rc; +} + +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/MbedTlsResumptionServer.h b/Tests/MbedTlsIntegration/MbedTlsResumptionServer.h new file mode 100644 index 00000000..b8276c44 --- /dev/null +++ b/Tests/MbedTlsIntegration/MbedTlsResumptionServer.h @@ -0,0 +1,55 @@ +#ifndef MBEDTLSRESUMPTIONSERVER_H +#define MBEDTLSRESUMPTIONSERVER_H + +#include +#include + +#include "ExternC.h" + +struct MbedTlsTestCert; + +EXTERN_C_BEGIN + + struct MbedTlsResumptionServer; + + struct MbedTlsResumptionServerConfig + { + int ServerFds[2]; /* the two socketpair server ends; ownership transfers to the server */ + const struct MbedTlsTestCert* ServerCert; /* server cert + matching key */ + mbedtls_ctr_drbg_context* Rng; /* shared with the test fixture */ + /* false: one ticket key shared across both connections -> the second + * handshake resumes. + * true: the ticket key is rotated before the second connection so the + * client's saved ticket no longer decrypts -> the server falls + * back to a full handshake (the non-resuming-peer case). */ + bool RotateTicketKeyBetweenConnections; + }; + + /* Spawns a worker that issues session tickets and drives two consecutive + * server-side handshakes (one per ServerFds entry), then reads one + * application record on each. The server owns both fds from Create. */ + struct MbedTlsResumptionServer* MbedTlsResumptionServer_Create(const struct MbedTlsResumptionServerConfig* config); + + /* Joins the worker and tears down all mbedTLS state. */ + void MbedTlsResumptionServer_Destroy(struct MbedTlsResumptionServer * self); + + /* Joins the worker thread. Callable once; the getters below are valid + * afterwards. */ + void MbedTlsResumptionServer_Join(struct MbedTlsResumptionServer * self); + + /* True when both handshakes completed successfully. */ + bool MbedTlsResumptionServer_BothHandshakesSucceeded(struct MbedTlsResumptionServer * self); + + /* True when an application record was received on both connections — + * proves delivery survives whichever handshake path was taken. */ + bool MbedTlsResumptionServer_BothMessagesDelivered(struct MbedTlsResumptionServer * self); + + /* The observable: true when the SECOND handshake presented a ticket the + * server accepted (an abbreviated / resumed handshake). mbedTLS exposes no + * client-side SSL_session_reused() equivalent, so the server records this + * by wrapping its ticket-parse callback. */ + bool MbedTlsResumptionServer_SecondHandshakeResumed(struct MbedTlsResumptionServer * self); + +EXTERN_C_END + +#endif /* MBEDTLSRESUMPTIONSERVER_H */ diff --git a/Tests/MbedTlsIntegration/ReconnectingSocketStream.c b/Tests/MbedTlsIntegration/ReconnectingSocketStream.c new file mode 100644 index 00000000..821ac7cb --- /dev/null +++ b/Tests/MbedTlsIntegration/ReconnectingSocketStream.c @@ -0,0 +1,127 @@ +#include "ReconnectingSocketStream.h" + +#include +#include +#include +#include +#include + +#include "SolidSyslogStream.h" +#include "SolidSyslogStreamDefinition.h" + +struct SolidSyslogAddress; + +enum +{ + RECONNECTING_SOCKET_STREAM_MAX_FDS = 4 +}; + +struct ReconnectingSocketStream +{ + struct SolidSyslogStream Base; + int Fds[RECONNECTING_SOCKET_STREAM_MAX_FDS]; + int Count; + int NextIndex; /* index of the fd the next Open will consume */ + int CurrentFd; /* fd handed out by the most recent Open; -1 when closed */ +}; + +static bool ReconnectingSocketStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool ReconnectingSocketStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static SolidSyslogSsize ReconnectingSocketStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void ReconnectingSocketStream_Close(struct SolidSyslogStream* self); + +struct SolidSyslogStream* ReconnectingSocketStream_Create(const int* fds, int count) +{ + struct ReconnectingSocketStream* stream = + (struct ReconnectingSocketStream*) calloc(1, sizeof(struct ReconnectingSocketStream)); + stream->Base.Open = ReconnectingSocketStream_Open; + stream->Base.Send = ReconnectingSocketStream_Send; + stream->Base.Read = ReconnectingSocketStream_Read; + stream->Base.Close = ReconnectingSocketStream_Close; + stream->Count = (count < RECONNECTING_SOCKET_STREAM_MAX_FDS) ? count : RECONNECTING_SOCKET_STREAM_MAX_FDS; + for (int i = 0; i < stream->Count; i++) + { + stream->Fds[i] = fds[i]; + } + stream->NextIndex = 0; + stream->CurrentFd = -1; + return &stream->Base; +} + +void ReconnectingSocketStream_Destroy(struct SolidSyslogStream* self) +{ + struct ReconnectingSocketStream* stream = (struct ReconnectingSocketStream*) self; + ReconnectingSocketStream_Close(self); + /* Close any fds the test never consumed (e.g. a test that failed before + * the second Open) so the harness leaks no descriptors. */ + for (int i = stream->NextIndex; i < stream->Count; i++) + { + if (stream->Fds[i] >= 0) + { + close(stream->Fds[i]); + stream->Fds[i] = -1; + } + } + free(stream); +} + +static bool ReconnectingSocketStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +{ + (void) addr; + struct ReconnectingSocketStream* stream = (struct ReconnectingSocketStream*) self; + bool ok = false; + if (stream->NextIndex < stream->Count) + { + stream->CurrentFd = stream->Fds[stream->NextIndex]; + stream->NextIndex++; + ok = true; + } + return ok; +} + +static bool ReconnectingSocketStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +{ + struct ReconnectingSocketStream* stream = (struct ReconnectingSocketStream*) self; + const unsigned char* bytes = (const unsigned char*) buffer; + size_t remaining = size; + bool ok = true; + while ((remaining > 0) && ok) + { + ssize_t n = send(stream->CurrentFd, bytes, remaining, 0); + if (n <= 0) + { + ok = false; + } + else + { + bytes += n; + remaining -= (size_t) n; + } + } + return ok; +} + +static SolidSyslogSsize ReconnectingSocketStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) +{ + /* Same EOF / would-block mapping as SocketStream: recv == 0 means the peer + * closed (fatal -> -1); the Stream contract reserves 0 for would-block, + * which a blocking socketpair never produces. */ + struct ReconnectingSocketStream* stream = (struct ReconnectingSocketStream*) self; + ssize_t n = recv(stream->CurrentFd, buffer, size, 0); + SolidSyslogSsize result = (SolidSyslogSsize) n; + if (n == 0) + { + result = -1; + } + return result; +} + +static void ReconnectingSocketStream_Close(struct SolidSyslogStream* self) +{ + struct ReconnectingSocketStream* stream = (struct ReconnectingSocketStream*) self; + if (stream->CurrentFd >= 0) + { + close(stream->CurrentFd); + stream->CurrentFd = -1; + } +} diff --git a/Tests/MbedTlsIntegration/ReconnectingSocketStream.h b/Tests/MbedTlsIntegration/ReconnectingSocketStream.h new file mode 100644 index 00000000..3b1ff1e4 --- /dev/null +++ b/Tests/MbedTlsIntegration/ReconnectingSocketStream.h @@ -0,0 +1,23 @@ +#ifndef RECONNECTINGSOCKETSTREAM_H +#define RECONNECTINGSOCKETSTREAM_H + +#include "ExternC.h" + +struct SolidSyslogStream; + +EXTERN_C_BEGIN + + /* A SolidSyslogStream wrapper that models a *reconnectable* transport for + * the session-resumption integration tests. Unlike SocketStream (one fixed + * fd, no-op Open), each Open consumes the next fd from a caller-supplied + * queue, so the library's real Close -> Open fail-fast reconnect lands on a + * fresh connection — exactly what session resumption needs to be observed + * end-to-end. The fds are pre-connected socketpair ends (no TCP ports), so + * the harness stays deterministic. Ownership of the fds transfers to the + * stream: Close closes the current one, Destroy closes any still open. */ + struct SolidSyslogStream* ReconnectingSocketStream_Create(const int* fds, int count); + void ReconnectingSocketStream_Destroy(struct SolidSyslogStream * self); + +EXTERN_C_END + +#endif /* RECONNECTINGSOCKETSTREAM_H */ diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index 4eb64f57..122b2ec5 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -8,8 +8,10 @@ extern "C" #include #include +#include "MbedTlsResumptionServer.h" #include "MbedTlsTestCert.h" #include "MbedTlsTestServer.h" +#include "ReconnectingSocketStream.h" #include "SocketStream.h" #include "SolidSyslogMbedTlsStream.h" #include "AddressFake.h" @@ -304,3 +306,180 @@ TEST(SolidSyslogMbedTlsStreamIntegration, BinaryLinksAgainstRealLibMbedTls) const unsigned int major = (mbedtls_version_get_number() >> 24) & 0xFFU; LONGS_EQUAL(3, major); } + +/* ------------------------------------------------------------------------- + * Session resumption (S26.04). Drives two consecutive handshakes on the SAME + * SolidSyslogMbedTlsStream instance — the library's real Close -> Open + * fail-fast reconnect — against a ticket-issuing server. The observable is + * server-side: mbedTLS has no client-side SSL_session_reused(), so the server + * wraps its ticket-parse callback to report when a presented ticket was + * accepted. A reconnecting transport feeds the stream a fresh socketpair on + * each Open; the TLS layer is still wired straight to the oracle (no + * production TCP stream in the loop). + * ------------------------------------------------------------------------- */ + +namespace +{ +void SetRecvTimeout(int fd, int seconds) +{ + struct timeval rcvTimeout = {seconds, 0}; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &rcvTimeout, sizeof(rcvTimeout)); +} +} // namespace + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsStreamResumption) +{ + mbedtls_entropy_context entropy = {}; + mbedtls_ctr_drbg_context rng = {}; + int clientFds[2] = {-1, -1}; + int serverFds[2] = {-1, -1}; + struct MbedTlsTestCert trustedCa = {}; + struct MbedTlsTestCert serverCert = {}; + struct MbedTlsResumptionServer* server = nullptr; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogStream* tlsStream = nullptr; + struct SolidSyslogAddress* addr = nullptr; + + void setup() override + { + addr = AddressFake_Get(); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&rng); + const unsigned char pers[] = "mbedtls-resumption-test"; + mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &entropy, pers, sizeof(pers) - 1U); + + /* Two pre-connected socketpairs: one per handshake. The reconnecting + * transport consumes the client ends in order; the server thread + * services the server ends in order. No TCP ports -> deterministic. */ + int pairA[2] = {-1, -1}; + int pairB[2] = {-1, -1}; + socketpair(AF_UNIX, SOCK_STREAM, 0, pairA); + socketpair(AF_UNIX, SOCK_STREAM, 0, pairB); + clientFds[0] = pairA[0]; + clientFds[1] = pairB[0]; + serverFds[0] = pairA[1]; + serverFds[1] = pairB[1]; + for (int fd : {clientFds[0], clientFds[1], serverFds[0], serverFds[1]}) + { + SetRecvTimeout(fd, 5); + } + + 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 + { + /* tlsStream first: its Destroy drives the transport Close that closes + * the current client fd, so it must run before the transport is freed. */ + if (tlsStream != nullptr) + { + SolidSyslogMbedTlsStream_Destroy(tlsStream); + } + if (transport != nullptr) + { + ReconnectingSocketStream_Destroy(transport); + } + if (server != nullptr) + { + MbedTlsResumptionServer_Destroy(server); + } + MbedTlsTestCert_Destroy(&serverCert); + MbedTlsTestCert_Destroy(&trustedCa); + mbedtls_ctr_drbg_free(&rng); + mbedtls_entropy_free(&entropy); + } + + struct SolidSyslogMbedTlsStreamConfig BuildBaseConfig(struct SolidSyslogStream* clientTransport) + { + struct SolidSyslogMbedTlsStreamConfig cfg = {}; + cfg.Transport = clientTransport; + cfg.Sleep = NoOpSleep; + cfg.Rng = &rng; + cfg.CaChain = &trustedCa.Cert; + cfg.ServerName = TEST_SERVER_HOSTNAME; + return cfg; + } + + struct MbedTlsResumptionServer* StartServer(bool rotateTicketKey) + { + struct MbedTlsResumptionServerConfig serverConfig = {}; + serverConfig.ServerFds[0] = serverFds[0]; + serverConfig.ServerFds[1] = serverFds[1]; + serverConfig.ServerCert = &serverCert; + serverConfig.Rng = &rng; + serverConfig.RotateTicketKeyBetweenConnections = rotateTicketKey; + return MbedTlsResumptionServer_Create(&serverConfig); + } + + /* One Open -> Send -> (the test closes if reconnecting) cycle on the same + * stream. Returns whether the Open and Send both succeeded. */ + bool ConnectAndDeliver(const char* payload) const + { + bool opened = SolidSyslogStream_Open(tlsStream, addr); + bool sent = opened && SolidSyslogStream_Send(tlsStream, payload, 8U); + return opened && sent; + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsStreamResumption, SameStreamReconnectResumesTheTlsSession) + +{ + transport = ReconnectingSocketStream_Create(clientFds, 2); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + server = StartServer(/*rotateTicketKey=*/false); + + CHECK_TRUE_TEXT(ConnectAndDeliver("<13>one\n"), "first (full) handshake + send should succeed"); + SolidSyslogStream_Close(tlsStream); /* the fail-fast reconnect the library performs in production */ + CHECK_TRUE_TEXT(ConnectAndDeliver("<13>two\n"), "second (resumed) handshake + send should succeed"); + + MbedTlsResumptionServer_Join(server); + CHECK_TRUE_TEXT( + MbedTlsResumptionServer_BothHandshakesSucceeded(server), + "both server-side handshakes should complete" + ); + CHECK_TRUE_TEXT(MbedTlsResumptionServer_BothMessagesDelivered(server), "a record should arrive on each connection"); + CHECK_TRUE_TEXT( + MbedTlsResumptionServer_SecondHandshakeResumed(server), + "the second handshake must present a ticket the server accepts (abbreviated handshake)" + ); +} + +TEST(SolidSyslogMbedTlsStreamResumption, NonResumingPeerFallsBackToFullHandshakeAndStillDelivers) + +{ + transport = ReconnectingSocketStream_Create(clientFds, 2); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + /* Rotate the ticket key between connections so the client's saved ticket + * no longer decrypts — the resume offer is rejected. */ + server = StartServer(/*rotateTicketKey=*/true); + + CHECK_TRUE_TEXT(ConnectAndDeliver("<13>one\n"), "first handshake + send should succeed"); + SolidSyslogStream_Close(tlsStream); + CHECK_TRUE_TEXT(ConnectAndDeliver("<13>two\n"), "second handshake + send should still succeed"); + + MbedTlsResumptionServer_Join(server); + CHECK_TRUE_TEXT(MbedTlsResumptionServer_BothHandshakesSucceeded(server), "both handshakes should complete"); + CHECK_TRUE_TEXT( + MbedTlsResumptionServer_BothMessagesDelivered(server), + "delivery must survive the full-handshake fallback" + ); + CHECK_FALSE_TEXT( + MbedTlsResumptionServer_SecondHandshakeResumed(server), + "with a rotated ticket key the second handshake must NOT resume" + ); +} diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index 5d4847ee..9f48e463 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -112,6 +112,27 @@ static mbedtls_ssl_config* lastSslConfOwnCertConfigArg; static mbedtls_x509_crt* lastSslConfOwnCertCertArg; static mbedtls_pk_context* lastSslConfOwnCertKeyArg; +/* mbedtls_ssl_session_init */ +static int sslSessionInitCallCount; +static mbedtls_ssl_session* lastSslSessionInitArg; + +/* mbedtls_ssl_get_session */ +static int sslGetSessionCallCount; +static const mbedtls_ssl_context* lastSslGetSessionContextArg; +static mbedtls_ssl_session* lastSslGetSessionSessionArg; +static int sslGetSessionReturn; + +/* mbedtls_ssl_set_session */ +static int sslSetSessionCallCount; +static mbedtls_ssl_context* lastSslSetSessionContextArg; +static const mbedtls_ssl_session* lastSslSetSessionSessionArg; +static int sslSetSessionReturn; +static int sslSetSessionHandshakeCountAtCall; + +/* mbedtls_ssl_session_free */ +static int sslSessionFreeCallCount; +static mbedtls_ssl_session* lastSslSessionFreeArg; + /* ------------------------------------------------------------------------- * Test accessors. * ------------------------------------------------------------------------- */ @@ -178,6 +199,19 @@ void MbedTlsFake_Reset(void) lastSslConfOwnCertConfigArg = NULL; lastSslConfOwnCertCertArg = NULL; lastSslConfOwnCertKeyArg = NULL; + sslSessionInitCallCount = 0; + lastSslSessionInitArg = NULL; + sslGetSessionCallCount = 0; + lastSslGetSessionContextArg = NULL; + lastSslGetSessionSessionArg = NULL; + sslGetSessionReturn = 0; + sslSetSessionCallCount = 0; + lastSslSetSessionContextArg = NULL; + lastSslSetSessionSessionArg = NULL; + sslSetSessionReturn = 0; + sslSetSessionHandshakeCountAtCall = 0; + sslSessionFreeCallCount = 0; + lastSslSessionFreeArg = NULL; } int MbedTlsFake_SslConfigInitCallCount(void) @@ -481,6 +515,71 @@ mbedtls_pk_context* MbedTlsFake_LastSslConfOwnCertKeyArg(void) return lastSslConfOwnCertKeyArg; } +int MbedTlsFake_SslSessionInitCallCount(void) +{ + return sslSessionInitCallCount; +} + +mbedtls_ssl_session* MbedTlsFake_LastSslSessionInitArg(void) +{ + return lastSslSessionInitArg; +} + +int MbedTlsFake_SslGetSessionCallCount(void) +{ + return sslGetSessionCallCount; +} + +const mbedtls_ssl_context* MbedTlsFake_LastSslGetSessionContextArg(void) +{ + return lastSslGetSessionContextArg; +} + +mbedtls_ssl_session* MbedTlsFake_LastSslGetSessionSessionArg(void) +{ + return lastSslGetSessionSessionArg; +} + +void MbedTlsFake_SetSslGetSessionReturn(int value) +{ + sslGetSessionReturn = value; +} + +int MbedTlsFake_SslSetSessionCallCount(void) +{ + return sslSetSessionCallCount; +} + +mbedtls_ssl_context* MbedTlsFake_LastSslSetSessionContextArg(void) +{ + return lastSslSetSessionContextArg; +} + +const mbedtls_ssl_session* MbedTlsFake_LastSslSetSessionSessionArg(void) +{ + return lastSslSetSessionSessionArg; +} + +void MbedTlsFake_SetSslSetSessionReturn(int value) +{ + sslSetSessionReturn = value; +} + +int MbedTlsFake_SslSetSessionHandshakeCountAtCall(void) +{ + return sslSetSessionHandshakeCountAtCall; +} + +int MbedTlsFake_SslSessionFreeCallCount(void) +{ + return sslSessionFreeCallCount; +} + +mbedtls_ssl_session* MbedTlsFake_LastSslSessionFreeArg(void) +{ + return lastSslSessionFreeArg; +} + /* ------------------------------------------------------------------------- * Link-interposed mbedTLS symbols. The test executable does not link * libmbedtls; the production code's calls to mbedtls_* resolve here. @@ -637,3 +736,32 @@ int mbedtls_ssl_set_hostname(mbedtls_ssl_context* ssl, const char* hostname) lastSslSetHostnameNameArg = hostname; return sslSetHostnameReturn; } + +void mbedtls_ssl_session_init(mbedtls_ssl_session* session) +{ + sslSessionInitCallCount++; + lastSslSessionInitArg = session; +} + +int mbedtls_ssl_get_session(const mbedtls_ssl_context* ssl, mbedtls_ssl_session* session) +{ + sslGetSessionCallCount++; + lastSslGetSessionContextArg = ssl; + lastSslGetSessionSessionArg = session; + return sslGetSessionReturn; +} + +int mbedtls_ssl_set_session(mbedtls_ssl_context* ssl, const mbedtls_ssl_session* session) +{ + sslSetSessionCallCount++; + lastSslSetSessionContextArg = ssl; + lastSslSetSessionSessionArg = session; + sslSetSessionHandshakeCountAtCall = sslHandshakeCallCount; + return sslSetSessionReturn; +} + +void mbedtls_ssl_session_free(mbedtls_ssl_session* session) +{ + sslSessionFreeCallCount++; + lastSslSessionFreeArg = session; +} diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index c8aa0398..da397ebf 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -8,6 +8,7 @@ struct mbedtls_ssl_config; struct mbedtls_ssl_context; +struct mbedtls_ssl_session; struct mbedtls_x509_crt; struct mbedtls_x509_crl; struct mbedtls_ctr_drbg_context; @@ -107,6 +108,29 @@ EXTERN_C_BEGIN const char* MbedTlsFake_LastSslSetHostnameNameArg(void); void MbedTlsFake_SetSslSetHostnameReturn(int value); + /* mbedtls_ssl_session_init (session-resumption save slot) */ + int MbedTlsFake_SslSessionInitCallCount(void); + struct mbedtls_ssl_session* MbedTlsFake_LastSslSessionInitArg(void); + + /* mbedtls_ssl_get_session (capture negotiated session after handshake) */ + int MbedTlsFake_SslGetSessionCallCount(void); + const struct mbedtls_ssl_context* MbedTlsFake_LastSslGetSessionContextArg(void); + struct mbedtls_ssl_session* MbedTlsFake_LastSslGetSessionSessionArg(void); + void MbedTlsFake_SetSslGetSessionReturn(int value); + + /* mbedtls_ssl_set_session (feed saved session back before next handshake) */ + int MbedTlsFake_SslSetSessionCallCount(void); + struct mbedtls_ssl_context* MbedTlsFake_LastSslSetSessionContextArg(void); + const struct mbedtls_ssl_session* MbedTlsFake_LastSslSetSessionSessionArg(void); + void MbedTlsFake_SetSslSetSessionReturn(int value); + /* Snapshot of the handshake call count at the moment set_session ran — + * lets a test prove the restore happens before the handshake it primes. */ + int MbedTlsFake_SslSetSessionHandshakeCountAtCall(void); + + /* mbedtls_ssl_session_free (release saved session on Destroy / recapture) */ + int MbedTlsFake_SslSessionFreeCallCount(void); + struct mbedtls_ssl_session* MbedTlsFake_LastSslSessionFreeArg(void); + /* mbedtls_ssl_conf_own_cert (mTLS client identity wiring) */ int MbedTlsFake_SslConfOwnCertCallCount(void); struct mbedtls_ssl_config* MbedTlsFake_LastSslConfOwnCertConfigArg(void); diff --git a/docs/integrating-mbedtls.md b/docs/integrating-mbedtls.md index f340a210..655d8cc3 100644 --- a/docs/integrating-mbedtls.md +++ b/docs/integrating-mbedtls.md @@ -195,6 +195,41 @@ shows the minimal config that satisfies the above for QEMU mps2-an385. --- +## Session resumption + +Under the fail-fast reconnect model, every transient peer hiccup tears the +TLS connection down and the next `Open` does a fresh handshake. A full +handshake costs ~2 RTTs plus the asymmetric crypto on both sides — the +dominant cost of the reconnect loop on a constrained target. + +The adapter resumes the TLS session across reconnects of the **same** +`SolidSyslogMbedTlsStream` instance. After a successful handshake it +captures the negotiated session (`mbedtls_ssl_get_session`) and feeds it +back (`mbedtls_ssl_set_session`) before the next handshake, so the peer can +issue an abbreviated handshake (~1 RTT, no asymmetric crypto). One session +is held per stream; the adapter does not multiplex destinations inside a +single stream. The saved session lives for the life of the instance and is +freed on `Destroy`. + +**Integrator requirement.** Resumption needs client-side session tickets +compiled into your Mbed TLS config: + +```c +#define MBEDTLS_SSL_SESSION_TICKETS /* client-side ticket storage */ +``` + +This is enabled by default in the upstream `mbedtls_config.h` (and in the +3.6.x tree used by the `integration-linux-mbedtls` check). If your trimmed +embedded config leaves it undefined, the adapter still works — resumption +simply never engages and every connect is a full handshake. + +**Best-effort, never a delivery precondition.** No saved session yet, a +peer that offers no ticket, or an expired / evicted ticket all fall back to +a full handshake transparently; the message is still delivered. Resumption +only ever makes the handshake cheaper — it never blocks the send path. + +--- + ## Reference integrations | Target | Adapter source | Mbed TLS config | Notes | diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 6654ca87..59ce38e1 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -53,7 +53,7 @@ misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c:49 misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c:40 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:44 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:114 -misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:94 +misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:102 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:82 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddress.c:10 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressPrivate.h:19 @@ -81,8 +81,8 @@ misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:145 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:207 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:139 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:147 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:271 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:283 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:315 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:327 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:138 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:353 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:373 @@ -183,5 +183,5 @@ misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:16 # D.013 — Rule 11.5: void* ↔ unsigned char* at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:308 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:327 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:352 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:371