From 6d33e9fd8c6fd1c53c40c0ef1ccaa4535606610a Mon Sep 17 00:00:00 2001 From: "Dr. Foulad" Date: Sun, 30 Aug 2026 02:03:26 +0300 Subject: [PATCH] fix: real peer verification in SecureClient, RFC 9110 no-body statuses SecureClient: - wolfSSL_check_domain_name() is armed for every non-insecure connection. SNI names the host but never bound the peer certificate to it, so a setCACert() connection still accepted any certificate signed by a trusted CA for any hostname. - wolfSSL_CTX_load_verify_buffer() failure now fails the connect outright instead of proceeding to a handshake against an empty trust store whose verify error is indistinguishable from a bad peer. SecureHttpClient: - 1xx/204/304 responses carry no body (RFC 9110 s6.4.1); read one and a kept-alive 304 with no framing headers stalls in readUntilClose() until the timeout. Required for conditional GETs (If-None-Match) against the GitHub API. --- .../SecureNet/include/SecureHttpClient.h | 11 +++++++++ libs/network/SecureNet/src/SecureClient.cpp | 24 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/libs/network/SecureNet/include/SecureHttpClient.h b/libs/network/SecureNet/include/SecureHttpClient.h index ceaa3bac..b5040d21 100644 --- a/libs/network/SecureNet/include/SecureHttpClient.h +++ b/libs/network/SecureNet/include/SecureHttpClient.h @@ -262,6 +262,17 @@ class SecureHttpClient { return -1; } + // RFC 9110 ยง6.4.1: 1xx, 204 and 304 responses never carry a body, + // regardless of what Content-Length or Transfer-Encoding they echo (a + // 304 replays the cached response's headers). Without this, a 304 with + // no framing headers would fall into readUntilClose() and stall a + // kept-alive connection until the timeout. + if (_status == 204 || _status == 304 || (_status >= 100 && _status < 200)) { + _bodyComplete = true; + if (!_reuse || !keepAlive) closeConnection(); + return _status; + } + // A close-delimited body (no framing) ends WITH the connection, so it can // never leave a reusable socket behind. // A 3xx body that is about to be followed is protocol plumbing, not diff --git a/libs/network/SecureNet/src/SecureClient.cpp b/libs/network/SecureNet/src/SecureClient.cpp index 25460735..7dac8edf 100644 --- a/libs/network/SecureNet/src/SecureClient.cpp +++ b/libs/network/SecureNet/src/SecureClient.cpp @@ -85,8 +85,15 @@ int SecureClient::connectWithMethod(const char* host, uint16_t port, void* metho if (_insecure) { wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_NONE, nullptr); } else if (_rootCA) { - wolfSSL_CTX_load_verify_buffer(ctx, reinterpret_cast(_rootCA), - strlen(_rootCA), WOLFSSL_FILETYPE_PEM); + // Fail closed on a CA buffer that doesn't parse: the context would + // otherwise proceed to the handshake with an empty trust store, and the + // resulting verify failure is indistinguishable from a real bad peer. + if (wolfSSL_CTX_load_verify_buffer(ctx, reinterpret_cast(_rootCA), strlen(_rootCA), + WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS) { + if (Serial) Serial.printf("[SecureClient] CA load failed (%s)\n", label); + stop(); + return 0; + } } wolfSSL_SetIORecv(ctx, wcRecv); wolfSSL_SetIOSend(ctx, wcSend); @@ -101,6 +108,19 @@ int SecureClient::connectWithMethod(const char* host, uint16_t port, void* metho wolfSSL_SetIOReadCtx(ssl, &_transport); wolfSSL_SetIOWriteCtx(ssl, &_transport); wolfSSL_UseSNI(ssl, WOLFSSL_SNI_HOST_NAME, host, strlen(host)); + if (!_insecure) { + // SNI names the host but does NOT bind the peer's certificate to it: + // without an explicit domain-name check, wolfSSL accepts any certificate + // signed by a trusted CA regardless of which hostname it was issued for, + // so a CA-verified connection would still be redirectable to any + // CA-certified attacker host. check_domain_name matches the connect host + // against the leaf's SANs/CN (wildcards included) during the handshake. + if (wolfSSL_check_domain_name(ssl, host) != WOLFSSL_SUCCESS) { + if (Serial) Serial.printf("[SecureClient] domain check setup failed (%s): %s\n", label, host); + stop(); + return 0; + } + } #if defined(WOLFSSL_TLS13) && defined(HAVE_CURVE25519) // MEMFIX-PORT: pin the TLS 1.3 key_share to X25519. wolfSSL's default is a // P-256 share, generated with fast-math bignums that WOLFSSL_SMALL_STACK