From 4c9809304a5e35661deb3936ddea6d3522f97fdb Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:18:49 +0100 Subject: [PATCH 1/9] fix: name the check that refused an OpenSSL handshake --- .../SolidSyslogOpenSslStreamErrors.h | 4 ++ .../OpenSsl/Source/SolidSyslogOpenSslStream.c | 37 +++++++++++- Tests/SolidSyslogOpenSslStreamTest.cpp | 60 +++++++++++++++++++ Tests/Support/OpenSslFake.c | 16 +++++ Tests/Support/OpenSslFake.h | 5 ++ 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Interface/SolidSyslogOpenSslStreamErrors.h b/Platform/OpenSsl/Interface/SolidSyslogOpenSslStreamErrors.h index 6c16dd37..4c31ed8b 100644 --- a/Platform/OpenSsl/Interface/SolidSyslogOpenSslStreamErrors.h +++ b/Platform/OpenSsl/Interface/SolidSyslogOpenSslStreamErrors.h @@ -31,6 +31,10 @@ SOLIDSYSLOG_EXTERN_C_BEGIN SOLIDSYSLOG_OPENSSL_STREAM_ERROR_NULL_CONFIG, SOLIDSYSLOG_OPENSSL_STREAM_ERROR_NULL_TRANSPORT, SOLIDSYSLOG_OPENSSL_STREAM_ERROR_NULL_SLEEP, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_NAME_MISMATCHED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID, SOLIDSYSLOG_OPENSSL_STREAM_ERROR_MAX /**< One past the last code; never emitted. Bounds the range for iteration. */ }; diff --git a/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c b/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c index 0252440a..65fe768e 100644 --- a/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,7 @@ static inline void OpenSslStream_LoadClientCredential( ); static inline bool OpenSslStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); static inline bool OpenSslStream_PerformHandshake(struct SolidSyslogOpenSslStream* self); +static inline enum SolidSyslogOpenSslStreamErrors OpenSslStream_RefusalDetail(struct SolidSyslogOpenSslStream* self); static inline SolidSyslogSsize OpenSslStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size); static inline void OpenSslStream_ReleaseBioMethod(struct SolidSyslogOpenSslStream* self); static inline void OpenSslStream_ReleaseHandshakeState(struct SolidSyslogOpenSslStream* self); @@ -550,7 +552,7 @@ static inline bool OpenSslStream_PerformHandshake(struct SolidSyslogOpenSslStrea OpenSslStream_Report( SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, - SOLIDSYSLOG_OPENSSL_STREAM_ERROR_HANDSHAKE_REJECTED + OpenSslStream_RefusalDetail(self) ); done = true; } @@ -573,6 +575,39 @@ static inline bool OpenSslStream_PerformHandshake(struct SolidSyslogOpenSslStrea return result; } +/* The verdict outlives the failed handshake - OpenSSL records it on the + * connection as path validation runs - so the refusal can name the check that + * produced it rather than the handshake that carried it. A verification failure + * this does not name individually reads as untrusted: the certificate did not + * validate, which is what the integrator has to act on. */ +static inline enum SolidSyslogOpenSslStreamErrors OpenSslStream_RefusalDetail(struct SolidSyslogOpenSslStream* self) +{ + enum SolidSyslogOpenSslStreamErrors detail = SOLIDSYSLOG_OPENSSL_STREAM_ERROR_HANDSHAKE_REJECTED; + long verdict = SSL_get_verify_result(self->Ssl); + if (verdict == X509_V_ERR_HOSTNAME_MISMATCH) + { + detail = SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_NAME_MISMATCHED; + } + else if (verdict == X509_V_ERR_CERT_HAS_EXPIRED) + { + detail = SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED; + } + else if (verdict == X509_V_ERR_CERT_NOT_YET_VALID) + { + detail = SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID; + } + else if (verdict != X509_V_OK) + { + detail = SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED; + } + else + { + /* Verification passed or never ran, so the refusal is a protocol or + * transport fault rather than one the peer's certificate explains. */ + } + return detail; +} + static inline bool OpenSslStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size) { struct SolidSyslogOpenSslStream* self = OpenSslStream_SelfFromBase(base); diff --git a/Tests/SolidSyslogOpenSslStreamTest.cpp b/Tests/SolidSyslogOpenSslStreamTest.cpp index dae6c536..5c5f63c7 100644 --- a/Tests/SolidSyslogOpenSslStreamTest.cpp +++ b/Tests/SolidSyslogOpenSslStreamTest.cpp @@ -747,6 +747,66 @@ TEST(SolidSyslogOpenSslStream, OpenReturnsFalseWhenHandshakeFails) ); } +TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateHasExpired) +{ + config.ServerName = "logs.example"; + ReCreateStreamWithUpdatedConfig(); + OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); + OpenSslFake_SetVerifyResult(X509_V_ERR_CERT_HAS_EXPIRED); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED + ); +} + +TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateIsNotYetValid) +{ + config.ServerName = "logs.example"; + ReCreateStreamWithUpdatedConfig(); + OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); + OpenSslFake_SetVerifyResult(X509_V_ERR_CERT_NOT_YET_VALID); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID + ); +} + +TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerNameDidNotMatch) +{ + config.ServerName = "logs.example"; + ReCreateStreamWithUpdatedConfig(); + OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); + OpenSslFake_SetVerifyResult(X509_V_ERR_HOSTNAME_MISMATCH); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_NAME_MISMATCHED + ); +} + +TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateIsNotTrusted) +{ + config.ServerName = "logs.example"; + ReCreateStreamWithUpdatedConfig(); + OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); + OpenSslFake_SetVerifyResult(X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED + ); +} + TEST(SolidSyslogOpenSslStream, OpenReturnsFalseWhenSet1HostFails) { config.ServerName = "logs.example"; diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index b2890919..6113c53f 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -11,6 +11,7 @@ #include #include #include +#include /* ------------------------------------------------------------------------- * Captured state - one section per OpenSSL API call. Tests read these via @@ -211,6 +212,9 @@ static int readReturnValue; static int getErrorCallCount; static int getErrorReturnValue; +/* SSL_get_verify_result */ +static long verifyResultValue; + /* BIO_set_flags / BIO_clear_flags */ static int bioSetFlagsCallCount; static int lastBioSetFlags; @@ -324,6 +328,7 @@ void OpenSslFake_Reset(void) readReturnValue = 0; getErrorCallCount = 0; getErrorReturnValue = 0; + verifyResultValue = X509_V_OK; bioSetFlagsCallCount = 0; lastBioSetFlags = 0; bioClearFlagsCallCount = 0; @@ -968,6 +973,17 @@ int OpenSslFake_GetErrorCallCount(void) return getErrorCallCount; } +long SSL_get_verify_result(const SSL* ssl) +{ + (void) ssl; + return verifyResultValue; +} + +void OpenSslFake_SetVerifyResult(long value) +{ + verifyResultValue = value; +} + /* BIO_set_flags / BIO_clear_flags are macros in OpenSSL that forward to these * non-inline functions. Faking the underlying calls lets the production code * use the standard idioms (BIO_set_retry_read, BIO_clear_retry_flags) while diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 573e1ec4..facc8b4e 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -42,6 +42,11 @@ SOLIDSYSLOG_EXTERN_C_BEGIN void OpenSslFake_SetGetErrorReturn(int err); int OpenSslFake_GetErrorCallCount(void); + /* SSL_get_verify_result - the certification-path verdict the adapter reads + * after a refused handshake. Resets to X509_V_OK, which is what OpenSSL + * reports when verification never ran or found nothing wrong. */ + void OpenSslFake_SetVerifyResult(long value); + int OpenSslFake_BioSetFlagsCallCount(void); int OpenSslFake_LastBioSetFlags(void); int OpenSslFake_BioClearFlagsCallCount(void); From e82c142b16df3e94d7ae5d2295107320e6ebfe33 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:22:31 +0100 Subject: [PATCH 2/9] fix: name the check that refused an Mbed TLS handshake --- .../SolidSyslogMbedTlsStreamErrors.h | 4 + .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 55 ++++++++- .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 116 ++++++++++++++++++ Tests/Support/MbedTlsFake.c | 13 ++ Tests/Support/MbedTlsFake.h | 5 + 5 files changed, 192 insertions(+), 1 deletion(-) diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsStreamErrors.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStreamErrors.h index 49456ff2..de713920 100644 --- a/Platform/MbedTls/Interface/SolidSyslogMbedTlsStreamErrors.h +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStreamErrors.h @@ -34,6 +34,10 @@ SOLIDSYSLOG_EXTERN_C_BEGIN SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_NULL_TRANSPORT, SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_NULL_SLEEP, SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_NULL_RNG, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID, SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_MAX /**< One past the last code; never emitted. Bounds the range for iteration. */ }; diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 98c5765d..205e10ba 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ enum HANDSHAKE_POLL_INTERVAL_MILLISECONDS = 1 }; +/* What mbedtls_ssl_get_verify_result answers when it holds no result. */ +static const uint32_t MbedTlsStream_VerifyResultUnavailable = 0xFFFFFFFFU; + struct SolidSyslogAddress; static uint32_t MbedTlsStream_NullHandshakeTimeoutGetter(void* context); @@ -45,6 +49,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 enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self); +static inline bool MbedTlsStream_HasUnnamedVerifyFailure(uint32_t verdict); 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); @@ -334,7 +340,7 @@ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStrea MbedTlsStream_Report( SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, - SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED + MbedTlsStream_RefusalDetail(self) ); done = true; } @@ -356,6 +362,53 @@ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStrea return result; } +/* The verdict outlives the failed handshake - mbedTLS records every fault it + * found on the session being negotiated - so the refusal can name the check that + * produced it rather than the handshake that carried it. The flags accumulate, + * so the order below is a precedence, and it is OpenSSL's: an untrusted chain is + * reported ahead of anything the certificate says about itself. */ +static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self) +{ + enum SolidSyslogMbedTlsStreamErrors detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED; + uint32_t verdict = mbedtls_ssl_get_verify_result(&self->SslContext); + if (verdict == MbedTlsStream_VerifyResultUnavailable) + { + /* No verdict to read, so the refusal is not the peer certificate's. */ + } + else if (MbedTlsStream_HasUnnamedVerifyFailure(verdict)) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED; + } + else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_CN_MISMATCH) != 0U) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED; + } + else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_EXPIRED) != 0U) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED; + } + else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_FUTURE) != 0U) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID; + } + else + { + /* Nothing the peer's certificate explains. */ + } + return detail; +} + +/* Every flag the cascade above does not name individually. A certificate that + * fails path validation for one of them is untrusted whichever it is, and the + * integrator's next step - replace the certificate, not the network - is the + * same. */ +static inline bool MbedTlsStream_HasUnnamedVerifyFailure(uint32_t verdict) +{ + const uint32_t named = (uint32_t) MBEDTLS_X509_BADCERT_CN_MISMATCH | (uint32_t) MBEDTLS_X509_BADCERT_EXPIRED | + (uint32_t) MBEDTLS_X509_BADCERT_FUTURE; + return (verdict & ~named) != 0U; +} + static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc) { return (rc == MBEDTLS_ERR_SSL_WANT_READ) || (rc == MBEDTLS_ERR_SSL_WANT_WRITE); diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 8c0b5f8d..7dda09fe 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -385,6 +385,122 @@ TEST(SolidSyslogMbedTlsStream, OpenClosesTransportAndFreesSslStateWhenHandshakeF ); } +TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateHasExpired) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_EXPIRED); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED + ); +} + +TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotYetValid) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_FUTURE); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID + ); +} + +TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerNameDidNotMatch) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_CN_MISMATCH); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED + ); +} + +TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotTrusted) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_NOT_TRUSTED); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED + ); +} + +/* mbedTLS accumulates every fault it found into one bitmask, so a compound + * verdict has to resolve to a single reason. An untrusted chain wins: it is what + * OpenSSL reports for the same certificate, because path building fails there + * before any date is examined. The two adapters therefore agree on a compound + * fault as well as on a single one. */ +TEST(SolidSyslogMbedTlsStream, OpenReportsAnUntrustedChainAheadOfTheDatesOnIt) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_EXPIRED); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED + ); +} + +/* A verification failure with no name of its own - here a key too weak for the + * profile - still tells the integrator the certificate is the fault rather than + * the network, which is the whole point of naming the check. */ +TEST(SolidSyslogMbedTlsStream, OpenReportsAVerificationFailureItCannotNameAsUntrusted) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_BAD_KEY); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED + ); +} + +/* mbedTLS answers 0xFFFFFFFF when it has no verdict to give. Every flag reads as + * set, so it must be recognised rather than mapped, or a refusal with no + * certificate behind it would be reported as an untrusted one. */ +TEST(SolidSyslogMbedTlsStream, OpenReportsAPlainRejectionWhenNoVerdictIsAvailable) +{ + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_BAD_INPUT_DATA); + MbedTlsFake_SetSslVerifyResult(0xFFFFFFFFU); + + CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR( + transport, + SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, + SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED + ); +} + /* ------------------------------------------------------------------------- * Open failure unwind + error reporting (S26.02). Every failure path after * the first allocating operation must close the inner transport, free both diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index 3b5a084a..95ac748a 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -108,6 +108,7 @@ enum static int sslHandshakeCallCount; static mbedtls_ssl_context* lastSslHandshakeArg; static int sslHandshakeReturn; +static uint32_t sslVerifyResult; static int sslHandshakeReturnSequence[MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS]; static int sslHandshakeReturnSequenceLen; @@ -205,6 +206,7 @@ void MbedTlsFake_Reset(void) sslHandshakeCallCount = 0; lastSslHandshakeArg = NULL; sslHandshakeReturn = 0; + sslVerifyResult = 0; sslHandshakeReturnSequenceLen = 0; sslWriteCallCount = 0; lastSslWriteContextArg = NULL; @@ -388,6 +390,17 @@ mbedtls_ssl_context* MbedTlsFake_LastSslHandshakeArg(void) return lastSslHandshakeArg; } +uint32_t mbedtls_ssl_get_verify_result(const mbedtls_ssl_context* ssl) +{ + (void) ssl; + return sslVerifyResult; +} + +void MbedTlsFake_SetSslVerifyResult(uint32_t flags) +{ + sslVerifyResult = flags; +} + void MbedTlsFake_SetSslHandshakeReturn(int value) { sslHandshakeReturn = value; diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index 0a087c7b..552bc163 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -53,6 +53,11 @@ SOLIDSYSLOG_EXTERN_C_BEGIN * at MBEDTLSFAKE_MAX_HANDSHAKE_RETURNS (silently truncated). */ void MbedTlsFake_SetSslHandshakeReturnSequence(const int* values, int count); + /* mbedtls_ssl_get_verify_result - the accumulated MBEDTLS_X509_BADCERT_* + * flags the adapter reads after a refused handshake. Resets to 0, which is + * what mbedTLS reports when verification found nothing wrong. */ + void MbedTlsFake_SetSslVerifyResult(uint32_t flags); + int MbedTlsFake_SslWriteCallCount(void); struct mbedtls_ssl_context* MbedTlsFake_LastSslWriteContextArg(void); const unsigned char* MbedTlsFake_LastSslWriteBufArg(void); From 1d43c445c9199808fd2872a8613fdf03825b08b7 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:24:18 +0100 Subject: [PATCH 3/9] test: pin each refusal reason against the real TLS libraries --- Tests/MbedTlsIntegration/MbedTlsTestCert.c | 4 +- Tests/MbedTlsIntegration/MbedTlsTestCert.h | 5 ++ ...olidSyslogMbedTlsStreamIntegrationTest.cpp | 80 +++++++++++++++++++ ...olidSyslogOpenSslStreamIntegrationTest.cpp | 28 +++++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.c b/Tests/MbedTlsIntegration/MbedTlsTestCert.c index 76b4ed43..0a7ede61 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestCert.c +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.c @@ -101,7 +101,9 @@ static void WriteCertToDer( 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); + const char* validityFrom = (config->ValidityFrom != NULL) ? config->ValidityFrom : TEST_CERT_VALIDITY_FROM; + const char* validityTo = (config->ValidityTo != NULL) ? config->ValidityTo : TEST_CERT_VALIDITY_TO; + mbedtls_x509write_crt_set_validity(&crt, validityFrom, validityTo); mbedtls_x509write_crt_set_basic_constraints(&crt, config->IsCa, -1); diff --git a/Tests/MbedTlsIntegration/MbedTlsTestCert.h b/Tests/MbedTlsIntegration/MbedTlsTestCert.h index 75fc7dcf..a9efdd00 100644 --- a/Tests/MbedTlsIntegration/MbedTlsTestCert.h +++ b/Tests/MbedTlsIntegration/MbedTlsTestCert.h @@ -22,6 +22,11 @@ SOLIDSYSLOG_EXTERN_C_BEGIN const char* SubjectAltDns; /* SAN dnsName; NULL = no SAN */ int IsCa; /* 1 = mark BasicConstraints CA:TRUE */ const struct MbedTlsTestCert* Issuer; /* NULL = self-signed */ + /* "YYYYMMDDHHMMSS", as mbedtls_x509write_crt_set_validity takes them. + * NULL on either leaves the default window, which is open now and + * stays open past any plausible run of these tests. */ + const char* ValidityFrom; + const char* ValidityTo; }; /* Build a fresh RSA-2048 key + cert pair. The cert is parsed back into diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index a1d7a8a6..df002477 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -11,11 +11,17 @@ extern "C" #include "MbedTlsTestCert.h" #include "MbedTlsTestServer.h" #include "SocketStream.h" +#include "SolidSyslogError.h" #include "SolidSyslogMbedTlsStream.h" +#include "SolidSyslogMbedTlsStreamErrors.h" +#include "SolidSyslogPrival.h" #include "AddressFake.h" #include "SolidSyslogStream.h" } +#include "SolidSyslogErrorCategory.h" +#include "SolidSyslogTlsStreamCategories.h" + namespace { constexpr const char* TEST_SERVER_HOSTNAME = "syslog.example.com"; @@ -28,6 +34,30 @@ void NoOpSleep(int milliseconds) } } // namespace +/* This suite links no ErrorHandlerFake - that is the unit executable's, and this + * one builds against the real libmbedtls. Capturing the last event directly is + * enough to pin which code a real fault produces. */ +static int CapturedErrorCount; +static struct SolidSyslogErrorEvent LastCapturedError; + +static void CaptureError(void* context, const struct SolidSyslogErrorEvent* event) +{ + (void) context; + CapturedErrorCount++; + LastCapturedError = *event; +} + +/* Pins a refused handshake to the check that refused it, against the real + * libmbedtls rather than the fake's canned verdict. */ +#define CHECK_REFUSAL_REPORTED(expectedCode) \ + { \ + LONGS_EQUAL(1, CapturedErrorCount); \ + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, LastCapturedError.Severity); \ + POINTERS_EQUAL(&MbedTlsStreamErrorSource, LastCapturedError.Source); \ + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, LastCapturedError.Category); \ + LONGS_EQUAL((expectedCode), LastCapturedError.Detail); \ + } + // clang-format off TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) { @@ -44,6 +74,9 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) void setup() override { addr = AddressFake_Get(); + CapturedErrorCount = 0; + LastCapturedError = {}; + SolidSyslog_SetErrorHandler(CaptureError, nullptr); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&rng); const unsigned char pers[] = "mbedtls-integration-test"; @@ -75,6 +108,7 @@ TEST_GROUP(SolidSyslogMbedTlsStreamIntegration) void teardown() override { + SolidSyslog_SetErrorHandler(nullptr, nullptr); if (tlsStream != nullptr) { SolidSyslogMbedTlsStream_Destroy(tlsStream); @@ -186,6 +220,7 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertSignedByUn bool opened = SolidSyslogStream_Open(tlsStream, addr); CHECK_FALSE_TEXT(opened, "client-side handshake must fail when the server cert chains to an untrusted CA"); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED); MbedTlsTestCert_Destroy(&untrustedCa); } @@ -201,6 +236,51 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMat bool opened = SolidSyslogStream_Open(tlsStream, addr); CHECK_FALSE_TEXT(opened, "client-side handshake must fail when ServerName does not match the cert's SAN"); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED); +} + +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertHasExpired) + +{ + struct MbedTlsTestCert expiredCert = {}; + struct MbedTlsTestCertConfig expiredConfig = {}; + expiredConfig.SubjectName = TEST_SERVER_SUBJECT; + expiredConfig.SubjectAltDns = TEST_SERVER_HOSTNAME; + expiredConfig.Issuer = &trustedCa; + expiredConfig.ValidityFrom = "20240101000000"; + expiredConfig.ValidityTo = "20240102000000"; + MbedTlsTestCert_Create(&expiredConfig, &expiredCert, &rng); + + struct SolidSyslogStream* transport = StartServerWithCert(&expiredCert); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED); + + MbedTlsTestCert_Destroy(&expiredCert); +} + +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertIsNotYetValid) + +{ + struct MbedTlsTestCert futureCert = {}; + struct MbedTlsTestCertConfig futureConfig = {}; + futureConfig.SubjectName = TEST_SERVER_SUBJECT; + futureConfig.SubjectAltDns = TEST_SERVER_HOSTNAME; + futureConfig.Issuer = &trustedCa; + futureConfig.ValidityFrom = "20980101000000"; + futureConfig.ValidityTo = "20990101000000"; + MbedTlsTestCert_Create(&futureConfig, &futureCert, &rng); + + struct SolidSyslogStream* transport = StartServerWithCert(&futureCert); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID); + + MbedTlsTestCert_Destroy(&futureCert); } TEST(SolidSyslogMbedTlsStreamIntegration, MutualTlsHandshakeSucceedsWithClientCertSignedByTrustedCa) diff --git a/Tests/OpenSslIntegration/SolidSyslogOpenSslStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogOpenSslStreamIntegrationTest.cpp index f7668c24..d983db7b 100644 --- a/Tests/OpenSslIntegration/SolidSyslogOpenSslStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogOpenSslStreamIntegrationTest.cpp @@ -11,6 +11,7 @@ #include "AddressFake.h" #include "SolidSyslogError.h" #include "SolidSyslogErrorCategory.h" +#include "SolidSyslogTlsStreamCategories.h" #include "SolidSyslogPrival.h" #include "SolidSyslogStream.h" #include "SolidSyslogOpenSslStream.h" @@ -41,6 +42,17 @@ static void CaptureError(void* context, const struct SolidSyslogErrorEvent* even LastCapturedError = *event; } +/* Pins a refused handshake to the check that refused it, against the real + * libssl rather than the fake's canned verdict. */ +#define CHECK_REFUSAL_REPORTED(expectedCode) \ + { \ + LONGS_EQUAL(1, CapturedErrorCount); \ + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, LastCapturedError.Severity); \ + POINTERS_EQUAL(&OpenSslStreamErrorSource, LastCapturedError.Source); \ + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED, LastCapturedError.Category); \ + LONGS_EQUAL((expectedCode), LastCapturedError.Detail); \ + } + // clang-format off TEST_GROUP(OpenSslStreamIntegration) { @@ -167,6 +179,20 @@ TEST(OpenSslStreamIntegration, HandshakeRejectedWhenServerCertIsExpired) buildScenario(certConfig); CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED); +} + +TEST(OpenSslStreamIntegration, HandshakeRejectedWhenServerCertIsNotYetValid) +{ + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = LOCALHOST_SANS; + certConfig.notBefore = std::time(nullptr) + 3600; + certConfig.notAfter = std::time(nullptr) + 7200; + buildScenario(certConfig); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID); } TEST(OpenSslStreamIntegration, HandshakeRejectedWhenServerCertHostnameDoesNotMatch) @@ -178,6 +204,7 @@ TEST(OpenSslStreamIntegration, HandshakeRejectedWhenServerCertHostnameDoesNotMat buildScenario(certConfig); /* client.ServerName defaults to "localhost" */ CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_NAME_MISMATCHED); } TEST(OpenSslStreamIntegration, HandshakeRejectedWhenClientDoesNotTrustServerCert) @@ -198,6 +225,7 @@ TEST(OpenSslStreamIntegration, HandshakeRejectedWhenClientDoesNotTrustServerCert TlsTestCert_WritePemToFile(&untrusted, caPath); CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_OPENSSL_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED); TlsTestCert_Destroy(&untrusted); } From 2d7f1be1dad724f2d9f87ea7ad951f29811c2334 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:25:44 +0100 Subject: [PATCH 4/9] docs: retire the divergence both TLS pages named --- .../SolidSyslogMbedTlsStreamIntegrationTest.cpp | 16 ++++++++++++++++ docs/platforms/mbedtls/index.md | 14 ++++---------- docs/platforms/openssl/index.md | 9 +-------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp index df002477..d64e712a 100644 --- a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsStreamIntegrationTest.cpp @@ -239,6 +239,22 @@ TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerNameDoesNotMat CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED); } +/* No trust anchors at all. mbedtls_ssl_conf_ca_chain takes the NULL without + * complaint, so the fault only surfaces when the peer certificate finds no + * parent to chain to - an untrusted peer, which is not the same diagnosis as + * the anchors never having been configured. #753 covers that gap. */ +TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsAsUntrustedWhenNoTrustAnchorsAreConfigured) + +{ + struct SolidSyslogStream* transport = StartServerWithCert(&serverCert); + struct SolidSyslogMbedTlsStreamConfig config = BuildBaseConfig(transport); + config.CaChain = nullptr; + tlsStream = SolidSyslogMbedTlsStream_Create(&config); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + CHECK_REFUSAL_REPORTED(SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED); +} + TEST(SolidSyslogMbedTlsStreamIntegration, HandshakeFailsWhenServerCertHasExpired) { diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index a80a755c..50b7d961 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -56,7 +56,7 @@ claim can be checked against the directory. ## Where it differs from the contract -Five differences, each tracked. Read them before relying on the corresponding +Four differences, each tracked. Read them before relying on the corresponding obligation. ### A peer cannot be authorised by certificate fingerprint @@ -65,13 +65,6 @@ Only certification path validation is offered, so a deployment with no PKI has n way to pin the collector's certificate. Tracked as [#753](https://github.com/cososo-ltd/solid-syslog/issues/753). -### A refused connection does not say which check refused it - -An expired certificate, an untrusted chain and a name mismatch all surface as the -same handshake failure, so the report does not distinguish a certificate problem -from a network one. Tracked as -[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). - ### Credential material must stay parsed for the life of the stream The adapter binds the handles into its `ssl_config` on each connection and drops @@ -92,7 +85,8 @@ where the library allows one to be selected. Tracked as ### A missing trust chain is not reported as a configuration fault `mbedtls_ssl_conf_ca_chain` returns no status, so a configuration carrying no -trust anchors is accepted and the peer certificate then fails to verify. What is -reported is a refused handshake rather than the missing trust material. Tracked +trust anchors is accepted, and the fault surfaces only once the peer's +certificate finds nothing to chain to. What is reported is an untrusted peer +rather than the missing trust material. Tracked as [#753](https://github.com/cososo-ltd/solid-syslog/issues/753), which is where a peer authorised by fingerprint instead of by trust anchor is settled. diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index 984faca5..0aa5f022 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -36,7 +36,7 @@ by calling `SolidSyslogSender_Disconnect`. ## Where it differs from the contract -Five differences, each tracked. Read them before relying on the corresponding +Four differences, each tracked. Read them before relying on the corresponding obligation. ### A peer cannot be authorised by certificate fingerprint @@ -45,13 +45,6 @@ Only certification path validation is offered, so a deployment with no PKI has n way to pin the collector's certificate. Tracked as [#753](https://github.com/cososo-ltd/solid-syslog/issues/753). -### A refused connection does not say which check refused it - -An expired certificate, an untrusted chain and a name mismatch all surface as the -same handshake failure, so the report does not distinguish a certificate problem -from a network one. Tracked as -[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). - ### Credentials come from the filesystem, and only from there The adapter opens the PEM files itself, so material held in a TPM, a keyring or From 8f9e24fe31a0fc286fe6c49daa4e7408dded5497 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:31:56 +0100 Subject: [PATCH 5/9] chore: keep the unavailable-verdict constant at block scope (MISRA 8.9) --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 7 +++---- misra_suppressions.txt | 16 ++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 205e10ba..94be1574 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -30,9 +30,6 @@ enum HANDSHAKE_POLL_INTERVAL_MILLISECONDS = 1 }; -/* What mbedtls_ssl_get_verify_result answers when it holds no result. */ -static const uint32_t MbedTlsStream_VerifyResultUnavailable = 0xFFFFFFFFU; - struct SolidSyslogAddress; static uint32_t MbedTlsStream_NullHandshakeTimeoutGetter(void* context); @@ -370,8 +367,10 @@ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStrea static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self) { enum SolidSyslogMbedTlsStreamErrors detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED; + /* What mbedtls_ssl_get_verify_result answers when it holds no result. */ + const uint32_t verifyResultUnavailable = 0xFFFFFFFFU; uint32_t verdict = mbedtls_ssl_get_verify_result(&self->SslContext); - if (verdict == MbedTlsStream_VerifyResultUnavailable) + if (verdict == verifyResultUnavailable) { /* No verdict to read, so the refusal is not the peer certificate's. */ } diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 67797c6c..ad09398a 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -54,12 +54,12 @@ misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c:57 misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c:52 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:48 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:126 -misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:106 +misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:109 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:83 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:85 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:89 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:84 -misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c:99 +misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c:101 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddress.c:18 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressPrivate.h:27 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressPrivate.h:32 @@ -86,8 +86,8 @@ misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:158 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:220 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:151 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:159 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:371 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:383 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:423 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:435 # D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) # See docs/misra-deviations.md#d003 @@ -142,12 +142,12 @@ misra-c2012-5.7:Core/Source/SolidSyslogStreamSender.c:29 misra-c2012-5.7:Core/Source/SolidSyslogUdpPayload.c:9 misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:29 misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:40 -misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:28 +misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:29 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:25 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:28 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:26 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:26 -misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c:29 +misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c:30 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixResolver.c:28 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixDatagram.c:29 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixSleep.c:10 @@ -196,8 +196,8 @@ misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:24 # D.013 — Rule 11.5: void* <-> a byte pointer at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:408 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:426 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:460 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:478 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:146 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:358 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:378 From 427ccf602f2be0e5b4f8d40396aaacd410f3bf4b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:34:21 +0100 Subject: [PATCH 6/9] docs: state the precedence without naming another platform --- Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c | 5 +++-- Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 94be1574..8c0e5a9b 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -362,8 +362,9 @@ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStrea /* The verdict outlives the failed handshake - mbedTLS records every fault it * found on the session being negotiated - so the refusal can name the check that * produced it rather than the handshake that carried it. The flags accumulate, - * so the order below is a precedence, and it is OpenSSL's: an untrusted chain is - * reported ahead of anything the certificate says about itself. */ + * so the order below is a precedence: an untrusted chain is reported ahead of + * anything the certificate says about itself, because a certificate no anchor + * vouches for is not made acceptable by the dates it carries. */ static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self) { enum SolidSyslogMbedTlsStreamErrors detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED; diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 7dda09fe..1811dc3e 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -446,10 +446,10 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotTrusted) } /* mbedTLS accumulates every fault it found into one bitmask, so a compound - * verdict has to resolve to a single reason. An untrusted chain wins: it is what - * OpenSSL reports for the same certificate, because path building fails there - * before any date is examined. The two adapters therefore agree on a compound - * fault as well as on a single one. */ + * verdict has to resolve to a single reason. An untrusted chain wins, which is + * the reason a library that stops at the first failure reaches first - path + * building runs before any date is examined - so both TLS adapters agree on a + * compound fault as well as on a single one. */ TEST(SolidSyslogMbedTlsStream, OpenReportsAnUntrustedChainAheadOfTheDatesOnIt) { config.ServerName = "syslog.example.com"; From 58193023b46aa921eceeac1cd99ca2f739716d32 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:42:24 +0100 Subject: [PATCH 7/9] chore: renumber the suppressions the reword shifted --- misra_suppressions.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/misra_suppressions.txt b/misra_suppressions.txt index ad09398a..bb177002 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -86,8 +86,8 @@ misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:158 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:220 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:151 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:159 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:423 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:435 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:424 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:436 # D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) # See docs/misra-deviations.md#d003 @@ -196,8 +196,8 @@ misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:24 # D.013 — Rule 11.5: void* <-> a byte pointer at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:460 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:478 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:461 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:479 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:146 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:358 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:378 From f821b89c69f016db7f0da345ff532cbd67ffccb5 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 10:59:10 +0100 Subject: [PATCH 8/9] refactor: split the Mbed TLS verdict mapping so no two branches are empty --- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 64 +++++++++++-------- misra_suppressions.txt | 8 +-- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 8c0e5a9b..3223fef9 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -47,6 +47,8 @@ static inline bool MbedTlsStream_ConfigureExpectedHostname(struct SolidSyslogMbe static inline void MbedTlsStream_InstallTransportCallbacks(struct SolidSyslogMbedTlsStream* self); static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStream* self); static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self); +static inline bool MbedTlsStream_IsVerifyFailure(uint32_t verdict); +static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_DetailForVerifyFailure(uint32_t verdict); static inline bool MbedTlsStream_HasUnnamedVerifyFailure(uint32_t verdict); static inline bool MbedTlsStream_IsRetryableHandshakeRc(int rc); static inline bool MbedTlsStream_IsHandshakeBudgetExhausted(uint32_t totalSleptMs, uint32_t budgetMs); @@ -361,39 +363,49 @@ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStrea /* The verdict outlives the failed handshake - mbedTLS records every fault it * found on the session being negotiated - so the refusal can name the check that - * produced it rather than the handshake that carried it. The flags accumulate, - * so the order below is a precedence: an untrusted chain is reported ahead of - * anything the certificate says about itself, because a certificate no anchor - * vouches for is not made acceptable by the dates it carries. */ + * produced it rather than the handshake that carried it. */ static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_RefusalDetail(struct SolidSyslogMbedTlsStream* self) { enum SolidSyslogMbedTlsStreamErrors detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_HANDSHAKE_REJECTED; - /* What mbedtls_ssl_get_verify_result answers when it holds no result. */ - const uint32_t verifyResultUnavailable = 0xFFFFFFFFU; uint32_t verdict = mbedtls_ssl_get_verify_result(&self->SslContext); - if (verdict == verifyResultUnavailable) - { - /* No verdict to read, so the refusal is not the peer certificate's. */ - } - else if (MbedTlsStream_HasUnnamedVerifyFailure(verdict)) - { - detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED; - } - else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_CN_MISMATCH) != 0U) - { - detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED; - } - else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_EXPIRED) != 0U) + if (MbedTlsStream_IsVerifyFailure(verdict)) { - detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED; + detail = MbedTlsStream_DetailForVerifyFailure(verdict); } - else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_FUTURE) != 0U) - { - detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID; - } - else + return detail; +} + +/* Zero is mbedTLS for "nothing wrong" and 0xFFFFFFFF for "no result to give". + * Neither is a check the peer's certificate failed, so neither may be read as a + * set of flags - every flag reads as set in the second of them. */ +static inline bool MbedTlsStream_IsVerifyFailure(uint32_t verdict) +{ + const uint32_t verifyResultUnavailable = 0xFFFFFFFFU; + return (verdict != 0U) && (verdict != verifyResultUnavailable); +} + +/* The flags accumulate, so a compound verdict resolves by precedence: an + * untrusted chain is reported ahead of anything the certificate says about + * itself, because a certificate no anchor vouches for is not made acceptable by + * the dates it carries. */ +static inline enum SolidSyslogMbedTlsStreamErrors MbedTlsStream_DetailForVerifyFailure(uint32_t verdict) +{ + enum SolidSyslogMbedTlsStreamErrors detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_UNTRUSTED; + if (MbedTlsStream_HasUnnamedVerifyFailure(verdict) == false) { - /* Nothing the peer's certificate explains. */ + if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_CN_MISMATCH) != 0U) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_NAME_MISMATCHED; + } + else if ((verdict & (uint32_t) MBEDTLS_X509_BADCERT_EXPIRED) != 0U) + { + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_EXPIRED; + } + else + { + /* A named flag is set and the other two are not, so this is it. */ + detail = SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_PEER_CERTIFICATE_NOT_YET_VALID; + } } return detail; } diff --git a/misra_suppressions.txt b/misra_suppressions.txt index bb177002..67d2e09e 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -54,7 +54,7 @@ misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c:57 misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c:52 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:48 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:126 -misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:109 +misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:111 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:83 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:85 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:89 @@ -86,8 +86,8 @@ misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:158 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:220 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:151 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:159 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:424 misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:436 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:448 # D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) # See docs/misra-deviations.md#d003 @@ -196,8 +196,8 @@ misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:24 # D.013 — Rule 11.5: void* <-> a byte pointer at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:461 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:479 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:473 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:491 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:146 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:358 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:378 From c433a18965578b8c704a26fea212d059c56f55a4 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 23 Aug 2026 12:41:49 +0100 Subject: [PATCH 9/9] test: lift the refused-certificate arrangement into a fixture helper --- .../MbedTls/SolidSyslogMbedTlsStreamTest.cpp | 44 +++++++++---------- Tests/SolidSyslogOpenSslStreamTest.cpp | 36 +++++++-------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp index 1811dc3e..d863ac22 100644 --- a/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp +++ b/Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp @@ -164,6 +164,17 @@ TEST_GROUP(SolidSyslogMbedTlsStream) int seq[] = {errorCode}; MbedTlsFake_SetSslHandshakeReturnSequence(seq, 1); } + + /* Arrange a peer whose certificate mbedTLS refused for `flags`. ServerName is + * set so the refusal is the only error source - a NULL one would also emit + * the unverified-peer WARNING. */ + void ArrangeCertificateVerificationFailure(uint32_t flags) + { + config.ServerName = "syslog.example.com"; + ReCreateHandleWithUpdatedConfig(); + ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); + MbedTlsFake_SetSslVerifyResult(flags); + } }; // clang-format on @@ -387,10 +398,7 @@ TEST(SolidSyslogMbedTlsStream, OpenClosesTransportAndFreesSslStateWhenHandshakeF TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateHasExpired) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_EXPIRED); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_EXPIRED); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -402,10 +410,7 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateHasExpired) TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotYetValid) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_FUTURE); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_FUTURE); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -417,10 +422,7 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotYetValid) TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerNameDidNotMatch) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_CN_MISMATCH); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_CN_MISMATCH); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -432,10 +434,7 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerNameDidNotMatch) TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotTrusted) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_NOT_TRUSTED); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_NOT_TRUSTED); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -452,10 +451,7 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsThatThePeerCertificateIsNotTrusted) * compound fault as well as on a single one. */ TEST(SolidSyslogMbedTlsStream, OpenReportsAnUntrustedChainAheadOfTheDatesOnIt) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_EXPIRED); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_NOT_TRUSTED | MBEDTLS_X509_BADCERT_EXPIRED); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -470,10 +466,7 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsAnUntrustedChainAheadOfTheDatesOnIt) * the network, which is the whole point of naming the check. */ TEST(SolidSyslogMbedTlsStream, OpenReportsAVerificationFailureItCannotNameAsUntrusted) { - config.ServerName = "syslog.example.com"; - ReCreateHandleWithUpdatedConfig(); - ArrangePersistentHandshakeError(MBEDTLS_ERR_X509_CERT_VERIFY_FAILED); - MbedTlsFake_SetSslVerifyResult(MBEDTLS_X509_BADCERT_BAD_KEY); + ArrangeCertificateVerificationFailure(MBEDTLS_X509_BADCERT_BAD_KEY); CHECK_FALSE(SolidSyslogStream_Open(handle, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( @@ -488,6 +481,9 @@ TEST(SolidSyslogMbedTlsStream, OpenReportsAVerificationFailureItCannotNameAsUntr * certificate behind it would be reported as an untrusted one. */ TEST(SolidSyslogMbedTlsStream, OpenReportsAPlainRejectionWhenNoVerdictIsAvailable) { + /* Arranged by hand rather than through ArrangeCertificateVerificationFailure: + * a verdict is unavailable when certificate verification never ran, which + * pairs with a handshake that failed for some other reason. */ config.ServerName = "syslog.example.com"; ReCreateHandleWithUpdatedConfig(); ArrangePersistentHandshakeError(MBEDTLS_ERR_SSL_BAD_INPUT_DATA); diff --git a/Tests/SolidSyslogOpenSslStreamTest.cpp b/Tests/SolidSyslogOpenSslStreamTest.cpp index 5c5f63c7..de8f2366 100644 --- a/Tests/SolidSyslogOpenSslStreamTest.cpp +++ b/Tests/SolidSyslogOpenSslStreamTest.cpp @@ -132,6 +132,18 @@ TEST_GROUP(SolidSyslogOpenSslStream) ReCreateStreamWithUpdatedConfig(); } + /* Arrange a peer whose certificate OpenSSL refused with `verifyResult`. + * ServerName is set so the refusal is the only error source - a NULL one + * would also emit the unverified-peer WARNING. */ + void ArrangeCertificateVerificationFailure(long verifyResult) + { + config.ServerName = "logs.example"; + ReCreateStreamWithUpdatedConfig(); + OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); + OpenSslFake_SetVerifyResult(verifyResult); + } + /* Drive the registered BIO read callback with the given transport return - collapses the open + set-return + grab-callback + invoke boilerplate. */ [[nodiscard]] int InvokeBioReadWithTransportReturn(SolidSyslogSsize transportReturn) const @@ -749,11 +761,7 @@ TEST(SolidSyslogOpenSslStream, OpenReturnsFalseWhenHandshakeFails) TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateHasExpired) { - config.ServerName = "logs.example"; - ReCreateStreamWithUpdatedConfig(); - OpenSslFake_SetConnectFails(true); - OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); - OpenSslFake_SetVerifyResult(X509_V_ERR_CERT_HAS_EXPIRED); + ArrangeCertificateVerificationFailure(X509_V_ERR_CERT_HAS_EXPIRED); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( transport, @@ -764,11 +772,7 @@ TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateHasExpired) TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateIsNotYetValid) { - config.ServerName = "logs.example"; - ReCreateStreamWithUpdatedConfig(); - OpenSslFake_SetConnectFails(true); - OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); - OpenSslFake_SetVerifyResult(X509_V_ERR_CERT_NOT_YET_VALID); + ArrangeCertificateVerificationFailure(X509_V_ERR_CERT_NOT_YET_VALID); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( transport, @@ -779,11 +783,7 @@ TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateIsNotYetValid) TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerNameDidNotMatch) { - config.ServerName = "logs.example"; - ReCreateStreamWithUpdatedConfig(); - OpenSslFake_SetConnectFails(true); - OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); - OpenSslFake_SetVerifyResult(X509_V_ERR_HOSTNAME_MISMATCH); + ArrangeCertificateVerificationFailure(X509_V_ERR_HOSTNAME_MISMATCH); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( transport, @@ -794,11 +794,7 @@ TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerNameDidNotMatch) TEST(SolidSyslogOpenSslStream, OpenReportsThatThePeerCertificateIsNotTrusted) { - config.ServerName = "logs.example"; - ReCreateStreamWithUpdatedConfig(); - OpenSslFake_SetConnectFails(true); - OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); - OpenSslFake_SetVerifyResult(X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY); + ArrangeCertificateVerificationFailure(X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); CHECK_OPEN_UNWOUND_WITH_ERROR( transport,