From da47028bc86070d8c98a9636f7ca13bcf08b893a Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 22 Aug 2026 09:55:04 -0700 Subject: [PATCH 01/21] ssl implementation --- .../AsyncWebServerSSL/AsyncWebServerSSL.ino | 106 ++++ src/AsyncTCP.cpp | 380 +++++++++++- src/AsyncTCP.h | 71 +++ src/AsyncTCP_TLS_Context.cpp | 574 ++++++++++++++++++ src/AsyncTCP_TLS_Context.h | 108 ++++ 5 files changed, 1235 insertions(+), 4 deletions(-) create mode 100644 examples/AsyncWebServerSSL/AsyncWebServerSSL.ino create mode 100644 src/AsyncTCP_TLS_Context.cpp create mode 100644 src/AsyncTCP_TLS_Context.h diff --git a/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino b/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino new file mode 100644 index 0000000..a0fbea0 --- /dev/null +++ b/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino @@ -0,0 +1,106 @@ +/* + * AsyncTCPSSL - AsyncWebServerSSL example + * + * ESP32 HTTPS server using ESPAsyncWebServer + AsyncTCPSSL. + * Serves a simple page and a JSON API over TLS 1.2 on port 443. + * + * Generate your own self-signed cert/key: + * openssl req -x509 -newkey rsa:2048 -nodes \ + * -keyout server.key -out server.pem -days 3650 \ + * -subj "/CN=esp32.local" + * + * Test with: + * curl -k https:///api + * + * Compile with: + * compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1 + */ + +#include +#include +#include + +const char *SSID = "YOUR_SSID"; +const char *PASSWORD = "YOUR_PASSWORD"; + +// Replace with your own PEM cert and key +static const char SERVER_CERT[] PROGMEM = R"EOF( +-----BEGIN CERTIFICATE----- +REPLACE_WITH_YOUR_CERT_PEM +-----END CERTIFICATE----- +)EOF"; + +static const char SERVER_KEY[] PROGMEM = R"EOF( +-----BEGIN RSA PRIVATE KEY----- +REPLACE_WITH_YOUR_KEY_PEM +-----END RSA PRIVATE KEY----- +)EOF"; + +AsyncWebServer server(443); +AsyncWebServer httpserver(80); + +const char index_html[] PROGMEM = R"rawliteral( + + + + + AsyncWebServerSSL + + + +

AsyncWebServerSSL

+

HTTPS is working!

+

/api - JSON endpoint

+ + +)rawliteral"; + +void setup() { + Serial.begin(115200); + delay(500); + + WiFi.begin(SSID, PASSWORD); + Serial.print("Connecting to WiFi"); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str()); + + // Serve a simple HTML page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", index_html); + }); + + // JSON API endpoint + server.on("/api", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + root["status"] = "ok"; + root["heap"] = ESP.getFreeHeap(); + root["uptime"] = millis() / 1000; + response->setLength(); + request->send(response); + }); + + // Start HTTPS server + server.beginSecure(SERVER_CERT, SERVER_KEY, NULL); + Serial.printf("[Server] HTTPS on port 443\n"); + Serial.printf("Test: curl -k https://%s/api\n", + WiFi.localIP().toString().c_str()); + + // Optional: HTTP-to-HTTPS redirect + httpserver.onNotFound([](AsyncWebServerRequest *request) { + request->redirect("https://" + WiFi.localIP().toString() + request->url()); + }); + httpserver.begin(); + Serial.printf("[Server] HTTP redirect on port 80\n"); +} + +void loop() { + delay(10000); +} diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 5a172e1..b811f47 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -5,6 +5,10 @@ #include "AsyncTCPLogging.h" #include "AsyncTCPSimpleIntrusiveList.h" +#if ASYNC_TCP_SSL_ENABLED +#include "AsyncTCP_TLS_Context.h" +#endif + /** * LibreTiny specific configurations */ @@ -777,6 +781,17 @@ AsyncClient::AsyncClient(tcp_pcb *pcb) _recv_cb_arg(0), _pb_cb(0), _pb_cb_arg(0), _timeout_cb(0), _timeout_cb_arg(0), _poll_cb(0), _poll_cb_arg(0), _ack_pcb(true), _tx_last_packet(0), _rx_timeout(0), _rx_last_ack(0), _ack_timeout(CONFIG_ASYNC_TCP_MAX_ACK_TIME), _connect_port(0) { _pcb = pcb; +#if ASYNC_TCP_SSL_ENABLED + _ssl_ctx = 0; + _ssl_handshake_done = false; + _ssl_timeout = SSL_HANDSHAKE_TIMEOUT; + _ssl_ca_cert = 0; + _ssl_ca_cert_len = 0; + _ssl_client_cert = 0; + _ssl_client_cert_len = 0; + _ssl_client_key = 0; + _ssl_client_key_len = 0; +#endif if (_pcb) { _rx_last_packet = millis(); _bind_tcp_callbacks(_pcb, this); @@ -784,6 +799,12 @@ AsyncClient::AsyncClient(tcp_pcb *pcb) } AsyncClient::~AsyncClient() { +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx) { + delete _ssl_ctx; + _ssl_ctx = 0; + } +#endif if (_pcb) { _close(); } @@ -937,6 +958,76 @@ bool AsyncClient::connect(const char *host, uint16_t port) { return false; } +#if ASYNC_TCP_SSL_ENABLED +bool AsyncClient::beginSecure(const char *host, uint16_t port, const char *rootCA, + const char *clientCert, const char *clientKey) { + return beginSecure(host, port, + (const unsigned char *)rootCA, (rootCA != NULL) ? strlen(rootCA) + 1 : 0, + (const unsigned char *)clientCert, (clientCert != NULL) ? strlen(clientCert) + 1 : 0, + (const unsigned char *)clientKey, (clientKey != NULL) ? strlen(clientKey) + 1 : 0); +} + +bool AsyncClient::beginSecure(const char *host, uint16_t port, + const unsigned char *rootCA, size_t rootCALen, + const unsigned char *clientCert, size_t clientCertLen, + const unsigned char *clientKey, size_t clientKeyLen) { + if (_ssl_ctx) { + async_tcp_log_d("already have SSL context"); + return false; + } + // Store SSL parameters — handshake will run in _connected() after TCP completes + _ssl_host = String(host); + _ssl_ca_cert = rootCA; + _ssl_ca_cert_len = rootCALen; + _ssl_client_cert = clientCert; + _ssl_client_cert_len = clientCertLen; + _ssl_client_key = clientKey; + _ssl_client_key_len = clientKeyLen; + return connect(host, port); +} + +void AsyncClient::feedSSLRxData(const unsigned char *data, size_t len) { + if (_ssl_ctx) { + _ssl_ctx->feedRxData(data, len); + } +} + +size_t AsyncClient::flushSSLTxData() { + if (_ssl_ctx) { + return _ssl_ctx->flushTxData(); + } + return 0; +} + +bool AsyncClient::hasSSLRxData() const { + if (_ssl_ctx) { + return _ssl_ctx->hasRxData(); + } + return false; +} + +int AsyncClient::sslRead(uint8_t *data, size_t len) { + if (_ssl_ctx) { + return _ssl_ctx->read(data, len); + } + return -1; +} + +int AsyncClient::sslWrite(const uint8_t *data, size_t len) { + if (_ssl_ctx) { + return _ssl_ctx->write(data, len); + } + return -1; +} + +int AsyncClient::runSSLHandshake() { + if (_ssl_ctx) { + return _ssl_ctx->runSSLHandshake(); + } + return -1; +} +#endif + void AsyncClient::close() { if (_pcb) { _tcp_recved(&_pcb, _rx_ack_len); @@ -967,6 +1058,16 @@ size_t AsyncClient::add(const char *data, size_t size, uint8_t apiflags) { if (!_pcb || size == 0 || data == NULL) { return 0; } +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx && _ssl_handshake_done) { + // SSL: encrypt via mbedtls, which calls BIO send -> tcp_write + int ret = _ssl_ctx->write((const uint8_t *)data, size); + if (ret > 0) { + return (size_t)ret; + } + return 0; + } +#endif size_t room = space(); if (!room) { return 0; @@ -1015,6 +1116,13 @@ void AsyncClient::ackPacket(struct pbuf *pb) { int8_t AsyncClient::_close() { // ets_printf("X: 0x%08x\n", (uint32_t)this); +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx) { + delete _ssl_ctx; + _ssl_ctx = 0; + _ssl_handshake_done = false; + } +#endif int8_t err = _tcp_close(&_pcb, this); // _pcb is now NULL if ((err == ERR_OK) && _discard_cb) { @@ -1035,6 +1143,59 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { } _tx_last_packet = 0; _rx_last_ack = 0; + +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_host.length() > 0 && !_ssl_ctx) { + // Create SSL context and start handshake + _ssl_ctx = new (std::nothrow) AsyncTCP_TLS_Context(); + if (!_ssl_ctx) { + async_tcp_log_e("failed to allocate SSL context"); + if (_error_cb) { + _error_cb(_error_cb_arg, this, -60); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + return ERR_ABRT; + } + int ret = _ssl_ctx->startSSLClient(_pcb, _ssl_host.c_str(), + _ssl_ca_cert, _ssl_ca_cert_len, + _ssl_client_cert, _ssl_client_cert_len, + _ssl_client_key, _ssl_client_key_len); + if (ret != 0) { + async_tcp_log_e("startSSLClient failed: %d", ret); + delete _ssl_ctx; + _ssl_ctx = 0; + if (_error_cb) { + _error_cb(_error_cb_arg, this, -60); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + return ERR_ABRT; + } + } + + if (_ssl_ctx) { + int ret = _ssl_ctx->runSSLHandshake(); + if (ret != 0) { + if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + async_tcp_log_e("SSL handshake failed: %d", ret); + if (_error_cb) { + _error_cb(_error_cb_arg, this, -60); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + return ERR_ABRT; + } + return ERR_OK; + } + _ssl_handshake_done = true; + async_tcp_log_d("SSL handshake completed"); + } +#endif + if (_connect_cb) { async_tcp_log_elapsed("onConnect", _connect_cb(_connect_cb_arg, this)); } @@ -1066,6 +1227,13 @@ int8_t AsyncClient::_lwip_fin(tcp_pcb *pcb, int8_t err) { // In Async Thread int8_t AsyncClient::_fin(tcp_pcb *pcb, int8_t err) { +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx) { + delete _ssl_ctx; + _ssl_ctx = 0; + _ssl_handshake_done = false; + } +#endif close(); return ERR_OK; } @@ -1079,6 +1247,73 @@ int8_t AsyncClient::_sent(tcp_pcb *pcb, uint16_t len) { } int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx && !_ssl_handshake_done) { + // During handshake: feed all received data into SSL rx buffer + while (pb != NULL) { + _rx_last_packet = millis(); + pbuf *b = pb; + pb = b->next; + b->next = NULL; + _ssl_ctx->feedRxData((const unsigned char *)b->payload, b->len); + if (_pcb) { + _tcp_recved(&_pcb, b->len); + } + pbuf_free(b); + } + // Try to continue handshake + int ret = _ssl_ctx->runSSLHandshake(); + if (ret == 0) { + _ssl_handshake_done = true; + async_tcp_log_d("SSL handshake completed (from _recv)"); + if (_connect_cb) { + async_tcp_log_elapsed("onConnect", _connect_cb(_connect_cb_arg, this)); + } + } else if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + // Still in progress, wait for more data + } else { + async_tcp_log_e("SSL handshake failed in _recv: %d", ret); + if (_error_cb) { + _error_cb(_error_cb_arg, this, -60); + } + if (_discard_cb) { + _discard_cb(_discard_cb_arg, this); + } + } + return ERR_OK; + } + + if (_ssl_ctx && _ssl_handshake_done) { + // SSL established: feed encrypted data, decrypt, deliver plaintext + while (pb != NULL) { + _rx_last_packet = millis(); + pbuf *b = pb; + pb = b->next; + b->next = NULL; + _ssl_ctx->feedRxData((const unsigned char *)b->payload, b->len); + // Ack data to TCP immediately — it's now buffered in the SSL context + if (_pcb) { + _tcp_recved(&_pcb, b->len); + } + pbuf_free(b); + } + // Decrypt all available plaintext + _ack_pcb = true; + uint8_t buf[256]; + int n; + while ((n = _ssl_ctx->read(buf, sizeof(buf))) > 0) { + if (_recv_cb) { + async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); + } + if (!_ack_pcb) { + _rx_ack_len += n; + } + } + return ERR_OK; + } +#endif + + // Non-SSL path (original code) while (pb != NULL) { _rx_last_packet = millis(); // we should not ack before we assimilate the data @@ -1115,6 +1350,35 @@ int8_t AsyncClient::_poll(tcp_pcb *pcb) { uint32_t now = millis(); +#if ASYNC_TCP_SSL_ENABLED + // SSL handshake in progress — continue it + if (_ssl_ctx && !_ssl_handshake_done) { + if ((now - _rx_last_packet) > _ssl_timeout) { + async_tcp_log_e("SSL handshake timeout"); + if (_error_cb) { + _error_cb(_error_cb_arg, this, -61); + } + _close(); + return ERR_OK; + } + int ret = _ssl_ctx->runSSLHandshake(); + if (ret == 0) { + _ssl_handshake_done = true; + async_tcp_log_d("SSL handshake completed (from _poll)"); + if (_connect_cb) { + async_tcp_log_elapsed("onConnect", _connect_cb(_connect_cb_arg, this)); + } + } else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + async_tcp_log_e("SSL handshake failed in _poll: %d", ret); + if (_error_cb) { + _error_cb(_error_cb_arg, this, -60); + } + _close(); + } + return ERR_OK; + } +#endif + // ACK Timeout if (_ack_timeout) { const uint32_t one_day = 86400000; @@ -1168,6 +1432,16 @@ bool AsyncClient::free() { } size_t AsyncClient::write(const char *data, size_t size, uint8_t apiflags) { +#if ASYNC_TCP_SSL_ENABLED + if (_ssl_ctx && _ssl_handshake_done) { + int ret = _ssl_ctx->write((const uint8_t *)data, size); + if (ret > 0) { + _tx_last_packet = millis(); + return (size_t)ret; + } + return 0; + } +#endif size_t will_send = add(data, size, apiflags); if (!will_send || !send()) { return 0; @@ -1442,6 +1716,8 @@ const char *AsyncClient::errorToString(int8_t error) { case ERR_CLSD: return "Connection closed"; case ERR_ARG: return "Illegal argument"; case -55: return "DNS failed"; + case -60: return "SSL handshake failed"; + case -61: return "SSL handshake timeout"; default: return "UNKNOWN"; } } @@ -1468,10 +1744,18 @@ const char *AsyncClient::stateToString() const { */ AsyncServer::AsyncServer(ip_addr_t addr, uint16_t port) - : _port(port), _addr(addr), _noDelay(false), _pcb(nullptr), _connect_cb(nullptr), _connect_cb_arg(nullptr) {} + : _port(port), _addr(addr), _noDelay(false), _pcb(nullptr), _connect_cb(nullptr), _connect_cb_arg(nullptr) +#if ASYNC_TCP_SSL_ENABLED + , _use_ssl(false), _cert(nullptr), _cert_len(0), _key(nullptr), _key_len(0), _ssl_file_cb(nullptr), _ssl_file_cb_arg(nullptr), _ssl_key_password(nullptr) +#endif + {} #ifdef ARDUINO -AsyncServer::AsyncServer(IPAddress addr, uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) { +AsyncServer::AsyncServer(IPAddress addr, uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) +#if ASYNC_TCP_SSL_ENABLED + , _use_ssl(false), _cert(0), _cert_len(0), _key(0), _key_len(0), _ssl_file_cb(0), _ssl_file_cb_arg(0), _ssl_key_password(0) +#endif +{ #if ESP_IDF_VERSION_MAJOR < 5 #if LWIP_IPV4 && LWIP_IPV6 _addr.type = IPADDR_TYPE_V4; @@ -1484,7 +1768,11 @@ AsyncServer::AsyncServer(IPAddress addr, uint16_t port) : _port(port), _noDelay( #endif } #if ESP_IDF_VERSION_MAJOR < 5 && __has_include() && LWIP_IPV6 -AsyncServer::AsyncServer(IPv6Address addr, uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) { +AsyncServer::AsyncServer(IPv6Address addr, uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) +#if ASYNC_TCP_SSL_ENABLED + , _use_ssl(false), _cert(0), _cert_len(0), _key(0), _key_len(0), _ssl_file_cb(0), _ssl_file_cb_arg(0), _ssl_key_password(0) +#endif +{ #if LWIP_IPV4 && LWIP_IPV6 _addr.type = IPADDR_TYPE_V6; #endif @@ -1494,7 +1782,11 @@ AsyncServer::AsyncServer(IPv6Address addr, uint16_t port) : _port(port), _noDela #endif #endif -AsyncServer::AsyncServer(uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) { +AsyncServer::AsyncServer(uint16_t port) : _port(port), _noDelay(false), _pcb(0), _connect_cb(0), _connect_cb_arg(0) +#if ASYNC_TCP_SSL_ENABLED + , _use_ssl(false), _cert(0), _cert_len(0), _key(0), _key_len(0), _ssl_file_cb(0), _ssl_file_cb_arg(0), _ssl_key_password(0) +#endif +{ #if LWIP_IPV4 && LWIP_IPV6 _addr.type = IPADDR_TYPE_ANY; _addr.u_addr.ip4.addr = INADDR_ANY; @@ -1611,6 +1903,33 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { } int8_t AsyncServer::_accepted(AsyncClient *client) { +#if ASYNC_TCP_SSL_ENABLED + if (_use_ssl && _cert && _key && client && client->pcb()) { + AsyncTCP_TLS_Context *ssl = new (std::nothrow) AsyncTCP_TLS_Context(); + if (ssl) { + int ret = ssl->startSSLServer(client->pcb(), _cert, _cert_len, _key, _key_len, _ssl_key_password); + if (ret == 0) { + client->_ssl_ctx = ssl; + // Trigger handshake immediately + int hr = ssl->runSSLHandshake(); + if (hr == 0) { + client->_ssl_handshake_done = true; + async_tcp_log_d("Server SSL handshake completed"); + } else if (hr != MBEDTLS_ERR_SSL_WANT_READ && hr != MBEDTLS_ERR_SSL_WANT_WRITE) { + async_tcp_log_e("Server SSL handshake failed: %d", hr); + delete ssl; + client->_ssl_ctx = 0; + } + // If WANT_READ/WAIT_WRITE, handshake continues in _poll() + } else { + async_tcp_log_e("startSSLServer failed: %d", ret); + delete ssl; + } + } else { + async_tcp_log_e("Failed to allocate SSL context for server"); + } + } +#endif if (_connect_cb) { async_tcp_log_elapsed("onClient", _connect_cb(_connect_cb_arg, client)); } @@ -1625,6 +1944,59 @@ bool AsyncServer::getNoDelay() const { return _noDelay; } +#if ASYNC_TCP_SSL_ENABLED +bool AsyncServer::beginSecure(const unsigned char *cert, size_t certLen, + const unsigned char *key, size_t keyLen) { + if (cert == NULL || key == NULL) { + async_tcp_log_e("SSL cert or key is NULL"); + return false; + } + _cert = cert; + _cert_len = certLen; + _key = key; + _key_len = keyLen; + _use_ssl = true; + begin(); + return _pcb != NULL; +} + +bool AsyncServer::beginSecure(const char *certPEM, const char *keyPEM) { + return beginSecure( + (const unsigned char *)certPEM, certPEM ? strlen(certPEM) + 1 : 0, + (const unsigned char *)keyPEM, keyPEM ? strlen(keyPEM) + 1 : 0); +} + +bool AsyncServer::beginSecure(const char *certPEM, const char *keyPEM, const char *password) { + _ssl_key_password = password; + return beginSecure(certPEM, keyPEM); +} + +void AsyncServer::setDefaultCertificate(const unsigned char *cert, size_t certLen) { + _cert = cert; + _cert_len = certLen; +} + +void AsyncServer::setDefaultKey(const unsigned char *key, size_t keyLen) { + _key = key; + _key_len = keyLen; +} + +void AsyncServer::setDefaultCertificatePEM(const char *certPEM) { + _cert = (const unsigned char *)certPEM; + _cert_len = certPEM ? strlen(certPEM) + 1 : 0; +} + +void AsyncServer::setDefaultKeyPEM(const char *keyPEM) { + _key = (const unsigned char *)keyPEM; + _key_len = keyPEM ? strlen(keyPEM) + 1 : 0; +} + +void AsyncServer::onSslFileRequest(AcSSlFileHandler cb, void *arg) { + _ssl_file_cb = cb; + _ssl_file_cb_arg = arg; +} +#endif + uint8_t AsyncServer::status() const { if (!_pcb) { return 0; diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 536008e..0dff753 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -7,6 +7,11 @@ #include "AsyncTCPVersion.h" #define ASYNCTCP_FORK_ESP32Async +// SSL support — default enabled, set to 0 before including this header to disable +#ifndef ASYNC_TCP_SSL_ENABLED +#define ASYNC_TCP_SSL_ENABLED 0 +#endif + #ifdef ARDUINO #include "IPAddress.h" #if __has_include() @@ -18,6 +23,10 @@ #include "lwip/ip_addr.h" #include +#if ASYNC_TCP_SSL_ENABLED +typedef std::function AcSSlFileHandler; +#endif + #ifndef LIBRETINY #include "sdkconfig.h" extern "C" { @@ -63,6 +72,11 @@ class AsyncClient; #define ASYNC_WRITE_FLAG_COPY 0x01 // will allocate new buffer to hold the data while sending (else will hold reference to the data given) #define ASYNC_WRITE_FLAG_MORE 0x02 // will not send PSH flag, meaning that there should be more data to be sent before the application should react. +#if ASYNC_TCP_SSL_ENABLED +#define SSL_HANDSHAKE_TIMEOUT 10000 +class AsyncTCP_TLS_Context; +#endif + typedef std::function AcConnectHandler; typedef std::function AcAckHandler; typedef std::function AcErrorHandler; @@ -268,6 +282,26 @@ class AsyncClient { return _pcb; } +#if ASYNC_TCP_SSL_ENABLED + // SSL support + bool beginSecure(const char *host, uint16_t port, const char *rootCA = NULL, + const char *clientCert = NULL, const char *clientKey = NULL); + bool beginSecure(const char *host, uint16_t port, + const unsigned char *rootCA, size_t rootCALen, + const unsigned char *clientCert = NULL, size_t clientCertLen = 0, + const unsigned char *clientKey = NULL, size_t clientKeyLen = 0); + bool ssl() const { return _ssl_ctx != 0; } + void setSSLReceiveTimeout(uint32_t timeout) { _ssl_timeout = timeout; } + uint32_t getSSLReceiveTimeout() const { return _ssl_timeout; } + AsyncTCP_TLS_Context *getSSLContext() { return _ssl_ctx; } + void feedSSLRxData(const unsigned char *data, size_t len); + size_t flushSSLTxData(); + bool hasSSLRxData() const; + int sslRead(uint8_t *data, size_t len); + int sslWrite(const uint8_t *data, size_t len); + int runSSLHandshake(); +#endif + protected: friend class AsyncTCP_detail; friend class AsyncServer; @@ -308,6 +342,19 @@ class AsyncClient { int8_t _fin(tcp_pcb *pcb, int8_t err); int8_t _lwip_fin(tcp_pcb *pcb, int8_t err); void _dns_found(ip_addr_t *ipaddr); +#if ASYNC_TCP_SSL_ENABLED + AsyncTCP_TLS_Context *_ssl_ctx; + uint32_t _ssl_timeout; + bool _ssl_handshake_done; + // Stored for deferred handshake in _connected() + String _ssl_host; + const unsigned char *_ssl_ca_cert; + size_t _ssl_ca_cert_len; + const unsigned char *_ssl_client_cert; + size_t _ssl_client_cert_len; + const unsigned char *_ssl_client_key; + size_t _ssl_client_key_len; +#endif }; class AsyncServer { @@ -328,6 +375,19 @@ class AsyncServer { bool getNoDelay() const; uint8_t status() const; +#if ASYNC_TCP_SSL_ENABLED + // SSL server support + bool beginSecure(const unsigned char *cert, size_t certLen, + const unsigned char *key, size_t keyLen); + bool beginSecure(const char *certPEM, const char *keyPEM); + bool beginSecure(const char *certPEM, const char *keyPEM, const char *password); + void setDefaultCertificate(const unsigned char *cert, size_t certLen); + void setDefaultKey(const unsigned char *key, size_t keyLen); + void setDefaultCertificatePEM(const char *certPEM); + void setDefaultKeyPEM(const char *keyPEM); + void onSslFileRequest(AcSSlFileHandler cb, void *arg); +#endif + protected: friend class AsyncTCP_detail; @@ -338,6 +398,17 @@ class AsyncServer { AcConnectHandler _connect_cb; void *_connect_cb_arg; +#if ASYNC_TCP_SSL_ENABLED + bool _use_ssl; + const unsigned char *_cert; + size_t _cert_len; + const unsigned char *_key; + size_t _key_len; + AcSSlFileHandler _ssl_file_cb; + void *_ssl_file_cb_arg; + const char *_ssl_key_password; +#endif + int8_t _accept(tcp_pcb *newpcb, int8_t err); int8_t _accepted(AsyncClient *client); }; diff --git a/src/AsyncTCP_TLS_Context.cpp b/src/AsyncTCP_TLS_Context.cpp new file mode 100644 index 0000000..45a311c --- /dev/null +++ b/src/AsyncTCP_TLS_Context.cpp @@ -0,0 +1,574 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// SSL/TLS support for AsyncTCP using mbedTLS over LwIP raw TCP (tcp_pcb) +// Custom BIO callbacks replace BSD socket mbedtls_net_send/mbedtls_net_recv + +#include +#include "AsyncTCPLogging.h" +#include +#include +#include + +extern "C" { +#include "lwip/tcp.h" +} + +#include "AsyncTCP_TLS_Context.h" + +#if ASYNC_TCP_SSL_ENABLED +#if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) +# warning "Please configure IDF framework to include mbedTLS -> Enable pre-shared-key ciphersuites and activate at least one cipher" +#else + +static const char *pers = "esp32-tls"; + +// From mbedtls/net_sockets.h — not included since we use custom LwIP BIO +#ifndef MBEDTLS_ERR_NET_SEND_FAILED +#define MBEDTLS_ERR_NET_SEND_FAILED -0x004E +#endif + +// DER cache — PEM parsed once, reused for all server connections +static unsigned char *_cached_cert_der = NULL; +static size_t _cached_cert_der_len = 0; +static unsigned char *_cached_key_der = NULL; +static size_t _cached_key_der_len = 0; + +static int _handle_error(int err) { + if (err == -30848) { + return err; + } +#ifdef MBEDTLS_ERROR_C + char error_buf[100]; + mbedtls_strerror(err, error_buf, 100); + async_tcp_log_e("(%d) %s", err, error_buf); +#else + async_tcp_log_e("code %d", err); +#endif + return err; +} + +#define handle_error(e) _handle_error(e) + +/* + * Custom LwIP BIO callbacks for mbedTLS + * These bridge mbedTLS's I/O with LwIP raw TCP (tcp_pcb). + * The void* ctx points to the AsyncTCP_TLS_Context instance. + */ + +static int _lwip_ssl_send(void *ctx, const unsigned char *buf, size_t len) { + AsyncTCP_TLS_Context *sslctx = (AsyncTCP_TLS_Context *)ctx; + if (!sslctx || !sslctx->pcb()) { + return MBEDTLS_ERR_NET_SEND_FAILED; + } + tcp_pcb *pcb = sslctx->pcb(); + + err_t err = tcp_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); + if (err == ERR_OK) { + tcp_output(pcb); + return (int)len; + } + if (err == ERR_MEM) { + return MBEDTLS_ERR_SSL_WANT_WRITE; + } + return MBEDTLS_ERR_NET_SEND_FAILED; +} + +static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { + AsyncTCP_TLS_Context *sslctx = (AsyncTCP_TLS_Context *)ctx; + if (!sslctx || !sslctx->hasRxData()) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return sslctx->read(buf, len); +} + +/* + * AsyncTCP_TLS_Context implementation + */ + +AsyncTCP_TLS_Context::AsyncTCP_TLS_Context(void) { + mbedtls_ssl_init(&ssl_ctx); + mbedtls_ssl_config_init(&ssl_conf); + mbedtls_ctr_drbg_init(&drbg_ctx); + mbedtls_entropy_init(&entropy_ctx); + _pcb = NULL; + _ssl_key_password = NULL; + _have_ca_cert = false; + _have_client_cert = false; + _have_client_key = false; + handshake_timeout = 120000; + handshake_start_time = 0; + + _ssl_rx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_RX_BUF_SIZE); + _ssl_rx_buf_len = 0; + _ssl_rx_pos = 0; + _ssl_rx_total = 0; + + _ssl_tx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_TX_BUF_SIZE); + _ssl_tx_buf_len = 0; + _ssl_tx_pos = 0; +} + +AsyncTCP_TLS_Context::~AsyncTCP_TLS_Context() { + _deleteHandshakeCerts(); + + async_tcp_log_v("~AsyncTCP_TLS_Context"); + + mbedtls_ssl_free(&ssl_ctx); + mbedtls_ssl_config_free(&ssl_conf); + mbedtls_ctr_drbg_free(&drbg_ctx); + mbedtls_entropy_free(&entropy_ctx); + + if (_ssl_rx_buf) { + free(_ssl_rx_buf); + _ssl_rx_buf = NULL; + } + if (_ssl_tx_buf) { + free(_ssl_tx_buf); + _ssl_tx_buf = NULL; + } +} + +void AsyncTCP_TLS_Context::feedRxData(const unsigned char *data, size_t len) { + if (!_ssl_rx_buf || len == 0) return; + size_t space = ASYNCTCP_TLS_RX_BUF_SIZE - _ssl_rx_buf_len; + if (len > space) len = space; + memcpy(_ssl_rx_buf + _ssl_rx_buf_len, data, len); + _ssl_rx_buf_len += len; + _ssl_rx_total += len; +} + +size_t AsyncTCP_TLS_Context::flushTxData(void) { + if (!_ssl_tx_buf || _ssl_tx_buf_len == 0 || !_pcb) return 0; + size_t sent = 0; + size_t remaining = _ssl_tx_buf_len - _ssl_tx_pos; + if (remaining > 0) { + err_t err = tcp_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, remaining, TCP_WRITE_FLAG_COPY); + if (err == ERR_OK) { + tcp_output(_pcb); + sent = remaining; + _ssl_tx_buf_len = 0; + _ssl_tx_pos = 0; + } else if (err == ERR_MEM) { + // TCP buffer full, try to send what we can + size_t space = tcp_sndbuf(_pcb); + if (space > 0) { + err_t err2 = tcp_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, space, TCP_WRITE_FLAG_COPY); + if (err2 == ERR_OK) { + tcp_output(_pcb); + sent = space; + _ssl_tx_pos += space; + } + } + } + } + if (sent == 0 && _ssl_tx_buf_len > 0 && _ssl_tx_pos >= _ssl_tx_buf_len) { + _ssl_tx_buf_len = 0; + _ssl_tx_pos = 0; + } + return sent; +} + +int AsyncTCP_TLS_Context::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { + return _startSSLClient(pcb, host_or_ip, + NULL, 0, + NULL, 0, + NULL, 0, + NULL, NULL, + true); +} + +int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const char *pskIdent, const char *psKey) { + return _startSSLClient(pcb, host_or_ip, + NULL, 0, + NULL, 0, + NULL, 0, + pskIdent, psKey, + false); +} + +int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const char *rootCABuff, + const char *cli_cert, + const char *cli_key) { + return startSSLClient(pcb, host_or_ip, + (const unsigned char *)rootCABuff, (rootCABuff != NULL) ? strlen(rootCABuff) + 1 : 0, + (const unsigned char *)cli_cert, (cli_cert != NULL) ? strlen(cli_cert) + 1 : 0, + (const unsigned char *)cli_key, (cli_key != NULL) ? strlen(cli_key) + 1 : 0); +} + +int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const unsigned char *rootCABuff, const size_t rootCABuff_len, + const unsigned char *cli_cert, const size_t cli_cert_len, + const unsigned char *cli_key, const size_t cli_key_len) { + return _startSSLClient(pcb, host_or_ip, + rootCABuff, rootCABuff_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len, + NULL, NULL, + false); +} + +int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const unsigned char *rootCABuff, const size_t rootCABuff_len, + const unsigned char *cli_cert, const size_t cli_cert_len, + const unsigned char *cli_key, const size_t cli_key_len, + const char *pskIdent, const char *psKey, + bool insecure) { + int ret; + + if (rootCABuff == NULL && pskIdent == NULL && psKey == NULL && !insecure) { + return -1; + } + + if (!pcb) { + return -1; + } + + async_tcp_log_v("Seeding the random number generator"); + mbedtls_entropy_init(&entropy_ctx); + + ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, + &entropy_ctx, (const unsigned char *)pers, strlen(pers)); + if (ret < 0) { + return handle_error(ret); + } + + async_tcp_log_v("Setting up the SSL/TLS structure..."); + + if ((ret = mbedtls_ssl_config_defaults(&ssl_conf, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { + return handle_error(ret); + } + + if (insecure) { + mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); + async_tcp_log_i("WARNING: Skipping SSL Verification. INSECURE!"); + } else if (rootCABuff != NULL) { + async_tcp_log_v("Loading CA cert"); + mbedtls_x509_crt_init(&ca_cert); + mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); + ret = mbedtls_x509_crt_parse(&ca_cert, rootCABuff, rootCABuff_len); + _have_ca_cert = true; + mbedtls_ssl_conf_ca_chain(&ssl_conf, &ca_cert, NULL); + if (ret < 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + } else if (pskIdent != NULL && psKey != NULL) { + async_tcp_log_v("Setting up PSK"); + if ((strlen(psKey) & 1) != 0 || strlen(psKey) > 2 * MBEDTLS_PSK_MAX_LEN) { + async_tcp_log_e("pre-shared key not valid hex or too long"); + return -1; + } + unsigned char psk[MBEDTLS_PSK_MAX_LEN]; + size_t psk_len = strlen(psKey) / 2; + for (size_t j = 0; j < strlen(psKey); j += 2) { + char c = psKey[j]; + if (c >= '0' && c <= '9') c -= '0'; + else if (c >= 'A' && c <= 'F') c -= 'A' - 10; + else if (c >= 'a' && c <= 'f') c -= 'a' - 10; + else return -1; + psk[j / 2] = c << 4; + c = psKey[j + 1]; + if (c >= '0' && c <= '9') c -= '0'; + else if (c >= 'A' && c <= 'F') c -= 'A' - 10; + else if (c >= 'a' && c <= 'f') c -= 'a' - 10; + else return -1; + psk[j / 2] |= c; + } + ret = mbedtls_ssl_conf_psk(&ssl_conf, psk, psk_len, + (const unsigned char *)pskIdent, strlen(pskIdent)); + if (ret != 0) { + async_tcp_log_e("mbedtls_ssl_conf_psk returned %d", ret); + return handle_error(ret); + } + } else { + return -1; + } + + if (!insecure && cli_cert != NULL && cli_key != NULL) { + mbedtls_x509_crt_init(&client_cert); + mbedtls_pk_init(&client_key); + + async_tcp_log_v("Loading CRT cert"); + ret = mbedtls_x509_crt_parse(&client_cert, cli_cert, cli_cert_len); + _have_client_cert = true; + if (ret < 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + async_tcp_log_v("Loading private key"); + ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); + _have_client_key = true; + if (ret != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + mbedtls_ssl_conf_own_cert(&ssl_conf, &client_cert, &client_key); + } + + async_tcp_log_v("Setting hostname for TLS session..."); + if ((ret = mbedtls_ssl_set_hostname(&ssl_ctx, host_or_ip)) != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); + + // Reduce buffer sizes to fit ESP32 heap + mbedtls_ssl_conf_max_frag_len(&ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); + + if ((ret = mbedtls_ssl_setup(&ssl_ctx, &ssl_conf)) != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + _pcb = pcb; + // Set BIO: ctx is this context (used by both send and recv callbacks) + mbedtls_ssl_set_bio(&ssl_ctx, this, _lwip_ssl_send, _lwip_ssl_recv, NULL); + handshake_start_time = 0; + + return 0; +} + +int AsyncTCP_TLS_Context::startSSLServer(tcp_pcb *pcb, + const unsigned char *server_cert, size_t server_cert_len, + const unsigned char *server_key, size_t server_key_len, + const char *password) { + int ret; + + if (server_cert == NULL || server_key == NULL || !pcb) { + return -1; + } + + _ssl_key_password = password; + + async_tcp_log_v("Seeding the random number generator (server)"); + mbedtls_entropy_init(&entropy_ctx); + + ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, + &entropy_ctx, (const unsigned char *)pers, strlen(pers)); + if (ret < 0) { + return handle_error(ret); + } + + async_tcp_log_v("Setting up the SSL/TLS structure (server)..."); + + if ((ret = mbedtls_ssl_config_defaults(&ssl_conf, + MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { + return handle_error(ret); + } + + // Force TLS 1.2 only + mbedtls_ssl_conf_min_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_max_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); + + // Disable renegotiation + mbedtls_ssl_conf_renegotiation(&ssl_conf, MBEDTLS_SSL_RENEGOTIATION_DISABLED); + + // Pin fast cipher suite — hardware-accelerated AES-GCM + SHA256 on ESP32 + static const int server_ciphersuites[] = { + MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + 0 + }; + mbedtls_ssl_conf_ciphersuites(&ssl_conf, server_ciphersuites); + + // Self-signed cert — no client certificate verification + mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); + + // Load server certificate — use DER cache if available + mbedtls_x509_crt_init(&client_cert); + + if (_cached_cert_der != NULL) { + async_tcp_log_v("Using cached cert DER (%u bytes)", (unsigned)_cached_cert_der_len); + ret = mbedtls_x509_crt_parse(&client_cert, _cached_cert_der, _cached_cert_der_len); + } else { + async_tcp_log_v("Parsing cert PEM -> DER (first connection, will cache)"); + mbedtls_pem_context pem; + mbedtls_pem_init(&pem); + size_t use_len = 0; + ret = mbedtls_pem_read_buffer(&pem, + "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", + server_cert, NULL, 0, &use_len); + if (ret == 0) { + const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &_cached_cert_der_len); + _cached_cert_der = (unsigned char *)malloc(_cached_cert_der_len); + if (_cached_cert_der) { + memcpy(_cached_cert_der, der_buf, _cached_cert_der_len); + } + ret = mbedtls_x509_crt_parse(&client_cert, der_buf, _cached_cert_der_len); + } + mbedtls_pem_free(&pem); + } + _have_client_cert = true; + if (ret < 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + // Load server private key — use DER cache if available + mbedtls_pk_init(&client_key); + + if (_cached_key_der != NULL) { + async_tcp_log_v("Using cached key DER (%u bytes)", (unsigned)_cached_key_der_len); + const unsigned char *pwd = (const unsigned char *)_ssl_key_password; + size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; + ret = mbedtls_pk_parse_key(&client_key, _cached_key_der, _cached_key_der_len, + pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); + } else { + async_tcp_log_v("Parsing key PEM -> DER (first connection, will cache)"); + mbedtls_pem_context pem; + mbedtls_pem_init(&pem); + size_t use_len = 0; + const unsigned char *pwd = (const unsigned char *)_ssl_key_password; + size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; + + const char *key_header, *key_footer; + if (strncmp((const char *)server_key, "-----BEGIN ENCRYPTED PRIVATE KEY-----", 37) == 0) { + key_header = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + key_footer = "-----END ENCRYPTED PRIVATE KEY-----"; + } else if (strncmp((const char *)server_key, "-----BEGIN PRIVATE KEY-----", 27) == 0) { + key_header = "-----BEGIN PRIVATE KEY-----"; + key_footer = "-----END PRIVATE KEY-----"; + } else { + key_header = "-----BEGIN RSA PRIVATE KEY-----"; + key_footer = "-----END RSA PRIVATE KEY-----"; + } + ret = mbedtls_pem_read_buffer(&pem, key_header, key_footer, + server_key, pwd, pwd_len, &use_len); + if (ret == 0) { + const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &_cached_key_der_len); + _cached_key_der = (unsigned char *)malloc(_cached_key_der_len); + if (_cached_key_der) { + memcpy(_cached_key_der, der_buf, _cached_key_der_len); + } + ret = mbedtls_pk_parse_key(&client_key, der_buf, _cached_key_der_len, + NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); + } + mbedtls_pem_free(&pem); + } + _have_client_key = true; + if (ret != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + mbedtls_ssl_conf_own_cert(&ssl_conf, &client_cert, &client_key); + + mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); + + mbedtls_ssl_conf_max_frag_len(&ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); + + if ((ret = mbedtls_ssl_setup(&ssl_ctx, &ssl_conf)) != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + _pcb = pcb; + mbedtls_ssl_set_bio(&ssl_ctx, this, _lwip_ssl_send, _lwip_ssl_recv, NULL); + handshake_start_time = 0; + + return 0; +} + +int AsyncTCP_TLS_Context::runSSLHandshake(void) { + int ret, flags; + + if (!_pcb) return -1; + + if (handshake_start_time == 0) handshake_start_time = millis(); + ret = mbedtls_ssl_handshake(&ssl_ctx); + if (ret != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + return handle_error(ret); + } + if ((millis() - handshake_start_time) > handshake_timeout) + return -1; + return ret; + } + + // Handshake completed, validate remote side if required... + if (_have_client_cert && _have_client_key) { + async_tcp_log_d("Protocol is %s Ciphersuite is %s", mbedtls_ssl_get_version(&ssl_ctx), mbedtls_ssl_get_ciphersuite(&ssl_ctx)); + if ((ret = mbedtls_ssl_get_record_expansion(&ssl_ctx)) >= 0) { + async_tcp_log_d("Record expansion is %d", ret); + } else { + async_tcp_log_w("Record expansion is unknown (compression)"); + } + } + + async_tcp_log_v("Verifying peer X.509 certificate..."); + + flags = mbedtls_ssl_get_verify_result(&ssl_ctx); + if (flags != 0) { + char buf[512]; + memset(buf, 0, sizeof(buf)); + mbedtls_x509_crt_verify_info(buf, sizeof(buf), " ! ", flags); + if (strstr(buf, "skipped") != NULL) { + async_tcp_log_v("Certificate verification was skipped (expected for self-signed server): %s", buf); + } else { + async_tcp_log_e("Failed to verify peer certificate! verification info: %s", buf); + _deleteHandshakeCerts(); + return handle_error(-1); + } + } else { + async_tcp_log_v("Certificate verified."); + } + + _deleteHandshakeCerts(); + async_tcp_log_v("Free internal heap after TLS %u", ESP.getFreeHeap()); + + return 0; +} + +int AsyncTCP_TLS_Context::write(const uint8_t *data, size_t len) { + if (!_pcb) return -1; + + int ret = mbedtls_ssl_write(&ssl_ctx, data, len); + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret < 0) { + return handle_error(ret); + } + return ret; +} + +int AsyncTCP_TLS_Context::read(uint8_t *data, size_t len) { + if (!_ssl_rx_buf || _ssl_rx_pos >= _ssl_rx_buf_len) return 0; + size_t avail = _ssl_rx_buf_len - _ssl_rx_pos; + size_t copy = (avail < len) ? avail : len; + memcpy(data, _ssl_rx_buf + _ssl_rx_pos, copy); + _ssl_rx_pos += copy; + // Reset buffer when all consumed (TCP ack already done in _recv) + if (_ssl_rx_pos >= _ssl_rx_buf_len) { + _ssl_rx_buf_len = 0; + _ssl_rx_pos = 0; + _ssl_rx_total = 0; + } + return (int)copy; +} + +void AsyncTCP_TLS_Context::_deleteHandshakeCerts(void) { + if (_have_ca_cert) { + async_tcp_log_v("Cleaning CA certificate."); + mbedtls_x509_crt_free(&ca_cert); + _have_ca_cert = false; + } + if (_have_client_cert) { + async_tcp_log_v("Cleaning client certificate."); + mbedtls_x509_crt_free(&client_cert); + _have_client_cert = false; + } + if (_have_client_key) { + async_tcp_log_v("Cleaning client certificate key."); + mbedtls_pk_free(&client_key); + _have_client_key = false; + } +} + +#endif +#endif // ASYNC_TCP_SSL_ENABLED diff --git a/src/AsyncTCP_TLS_Context.h b/src/AsyncTCP_TLS_Context.h new file mode 100644 index 0000000..6b4eb09 --- /dev/null +++ b/src/AsyncTCP_TLS_Context.h @@ -0,0 +1,108 @@ +#pragma once + +#if ASYNC_TCP_SSL_ENABLED + +#include "mbedtls/platform.h" +#include "mbedtls/debug.h" +#include "mbedtls/ssl.h" +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/error.h" +#include "mbedtls/pem.h" +#include "mbedtls/sha256.h" +#include "mbedtls/oid.h" + +struct tcp_pcb; + +#define ASYNCTCP_TLS_CAN_RETRY(r) (((r) == MBEDTLS_ERR_SSL_WANT_READ) || ((r) == MBEDTLS_ERR_SSL_WANT_WRITE)) +#define ASYNCTCP_TLS_EOF(r) (((r) == MBEDTLS_ERR_SSL_CONN_EOF) || ((r) == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY)) + +#define ASYNCTCP_TLS_RX_BUF_SIZE 4096 +#define ASYNCTCP_TLS_TX_BUF_SIZE 4096 + +class AsyncTCP_TLS_Context +{ +private: + mbedtls_ssl_context ssl_ctx; + mbedtls_ssl_config ssl_conf; + mbedtls_ctr_drbg_context drbg_ctx; + mbedtls_entropy_context entropy_ctx; + + mbedtls_x509_crt ca_cert; + mbedtls_x509_crt client_cert; + mbedtls_pk_context client_key; + bool _have_ca_cert; + bool _have_client_cert; + bool _have_client_key; + + unsigned long handshake_timeout; + unsigned long handshake_start_time; + + tcp_pcb *_pcb; + + // PEM password for encrypted private keys + const char *_ssl_key_password; + + // Per-connection encrypted data buffers for BIO callbacks + unsigned char *_ssl_rx_buf; + size_t _ssl_rx_buf_len; + size_t _ssl_rx_pos; + size_t _ssl_rx_total; // total bytes buffered, for tcp_recved() + + unsigned char *_ssl_tx_buf; + size_t _ssl_tx_buf_len; + size_t _ssl_tx_pos; + + int _startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const unsigned char *rootCABuff, const size_t rootCABuff_len, + const unsigned char *cli_cert, const size_t cli_cert_len, + const unsigned char *cli_key, const size_t cli_key_len, + const char *pskIdent, const char *psKey, + bool insecure); + + void _deleteHandshakeCerts(void); + +public: + AsyncTCP_TLS_Context(void); + virtual ~AsyncTCP_TLS_Context(); + + // Feed encrypted data from TCP into BIO buffer + void feedRxData(const unsigned char *data, size_t len); + + // Flush BIO tx buffer to TCP (returns bytes flushed, 0 if nothing to flush) + size_t flushTxData(void); + + // Check if BIO has buffered rx data available + bool hasRxData(void) const { return _ssl_rx_buf && (_ssl_rx_pos < _ssl_rx_buf_len); } + + // Public accessor for PCB (needed by BIO callbacks) + tcp_pcb *pcb() const { return _pcb; } + + int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); + + int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const char *pskIdent, const char *psKey); + + int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const char *rootCABuff, + const char *cli_cert, + const char *cli_key); + + int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, + const unsigned char *rootCABuff, const size_t rootCABuff_len, + const unsigned char *cli_cert, const size_t cli_cert_len, + const unsigned char *cli_key, const size_t cli_key_len); + + int startSSLServer(tcp_pcb *pcb, + const unsigned char *server_cert, size_t server_cert_len, + const unsigned char *server_key, size_t server_key_len, + const char *password = NULL); + + int runSSLHandshake(void); + + int write(const uint8_t *data, size_t len); + + int read(uint8_t *data, size_t len); +}; + +#endif // ASYNC_TCP_SSL_ENABLED From 378dbd5574159fe7b981fe341d80ee0a227ba9b9 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 02:40:05 -0700 Subject: [PATCH 02/21] debug and testing with ESPAsyncWebServer --- src/AsyncTCP.cpp | 25 +-- src/AsyncTCP.h | 6 +- ...yncTCP_TLS_Context.cpp => AsyncTCPTLS.cpp} | 162 ++++++++++++++---- src/{AsyncTCP_TLS_Context.h => AsyncTCPTLS.h} | 20 ++- src/AsyncTCPVersion.h | 2 +- 5 files changed, 160 insertions(+), 55 deletions(-) rename src/{AsyncTCP_TLS_Context.cpp => AsyncTCPTLS.cpp} (80%) rename src/{AsyncTCP_TLS_Context.h => AsyncTCPTLS.h} (84%) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index b811f47..50787d5 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -6,7 +6,7 @@ #include "AsyncTCPSimpleIntrusiveList.h" #if ASYNC_TCP_SSL_ENABLED -#include "AsyncTCP_TLS_Context.h" +#include "AsyncTCPTLS.h" #endif /** @@ -1008,7 +1008,7 @@ bool AsyncClient::hasSSLRxData() const { int AsyncClient::sslRead(uint8_t *data, size_t len) { if (_ssl_ctx) { - return _ssl_ctx->read(data, len); + return _ssl_ctx->sslRead(data, len); } return -1; } @@ -1147,7 +1147,7 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { #if ASYNC_TCP_SSL_ENABLED if (_ssl_host.length() > 0 && !_ssl_ctx) { // Create SSL context and start handshake - _ssl_ctx = new (std::nothrow) AsyncTCP_TLS_Context(); + _ssl_ctx = new (std::nothrow) AsyncTCPTLS(); if (!_ssl_ctx) { async_tcp_log_e("failed to allocate SSL context"); if (_error_cb) { @@ -1299,9 +1299,9 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { } // Decrypt all available plaintext _ack_pcb = true; - uint8_t buf[256]; + uint8_t buf[1024]; int n; - while ((n = _ssl_ctx->read(buf, sizeof(buf))) > 0) { + while ((n = _ssl_ctx->sslRead(buf, sizeof(buf))) > 0) { if (_recv_cb) { async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); } @@ -1309,6 +1309,7 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { _rx_ack_len += n; } } + _ssl_ctx->flushOutput(); return ERR_OK; } #endif @@ -1905,22 +1906,12 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { int8_t AsyncServer::_accepted(AsyncClient *client) { #if ASYNC_TCP_SSL_ENABLED if (_use_ssl && _cert && _key && client && client->pcb()) { - AsyncTCP_TLS_Context *ssl = new (std::nothrow) AsyncTCP_TLS_Context(); + AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); if (ssl) { int ret = ssl->startSSLServer(client->pcb(), _cert, _cert_len, _key, _key_len, _ssl_key_password); if (ret == 0) { client->_ssl_ctx = ssl; - // Trigger handshake immediately - int hr = ssl->runSSLHandshake(); - if (hr == 0) { - client->_ssl_handshake_done = true; - async_tcp_log_d("Server SSL handshake completed"); - } else if (hr != MBEDTLS_ERR_SSL_WANT_READ && hr != MBEDTLS_ERR_SSL_WANT_WRITE) { - async_tcp_log_e("Server SSL handshake failed: %d", hr); - delete ssl; - client->_ssl_ctx = 0; - } - // If WANT_READ/WAIT_WRITE, handshake continues in _poll() + async_tcp_log_d("Server SSL context ready, handshake will start on first poll"); } else { async_tcp_log_e("startSSLServer failed: %d", ret); delete ssl; diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 0dff753..e6a7741 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -74,7 +74,7 @@ class AsyncClient; #if ASYNC_TCP_SSL_ENABLED #define SSL_HANDSHAKE_TIMEOUT 10000 -class AsyncTCP_TLS_Context; +class AsyncTCPTLS; #endif typedef std::function AcConnectHandler; @@ -293,7 +293,7 @@ class AsyncClient { bool ssl() const { return _ssl_ctx != 0; } void setSSLReceiveTimeout(uint32_t timeout) { _ssl_timeout = timeout; } uint32_t getSSLReceiveTimeout() const { return _ssl_timeout; } - AsyncTCP_TLS_Context *getSSLContext() { return _ssl_ctx; } + AsyncTCPTLS *getSSLContext() { return _ssl_ctx; } void feedSSLRxData(const unsigned char *data, size_t len); size_t flushSSLTxData(); bool hasSSLRxData() const; @@ -343,7 +343,7 @@ class AsyncClient { int8_t _lwip_fin(tcp_pcb *pcb, int8_t err); void _dns_found(ip_addr_t *ipaddr); #if ASYNC_TCP_SSL_ENABLED - AsyncTCP_TLS_Context *_ssl_ctx; + AsyncTCPTLS *_ssl_ctx; uint32_t _ssl_timeout; bool _ssl_handshake_done; // Stored for deferred handshake in _connected() diff --git a/src/AsyncTCP_TLS_Context.cpp b/src/AsyncTCPTLS.cpp similarity index 80% rename from src/AsyncTCP_TLS_Context.cpp rename to src/AsyncTCPTLS.cpp index 45a311c..148daa0 100644 --- a/src/AsyncTCP_TLS_Context.cpp +++ b/src/AsyncTCPTLS.cpp @@ -12,7 +12,7 @@ extern "C" { #include "lwip/tcp.h" } -#include "AsyncTCP_TLS_Context.h" +#include "AsyncTCPTLS.h" #if ASYNC_TCP_SSL_ENABLED #if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) @@ -26,12 +26,23 @@ static const char *pers = "esp32-tls"; #define MBEDTLS_ERR_NET_SEND_FAILED -0x004E #endif +#ifndef SSL_HANDSHAKE_TIMEOUT +#define SSL_HANDSHAKE_TIMEOUT 10000 +#endif + // DER cache — PEM parsed once, reused for all server connections static unsigned char *_cached_cert_der = NULL; static size_t _cached_cert_der_len = 0; static unsigned char *_cached_key_der = NULL; static size_t _cached_key_der_len = 0; +void AsyncTCPTLS::_clear_DER_cache(void) { + if (_cached_cert_der) { free(_cached_cert_der); _cached_cert_der = NULL; } + _cached_cert_der_len = 0; + if (_cached_key_der) { free(_cached_key_der); _cached_key_der = NULL; } + _cached_key_der_len = 0; +} + static int _handle_error(int err) { if (err == -30848) { return err; @@ -48,22 +59,73 @@ static int _handle_error(int err) { #define handle_error(e) _handle_error(e) +#include "lwip/priv/tcpip_priv.h" + +typedef struct { + struct tcpip_api_call_data call; + tcp_pcb *pcb; + const void *data; + size_t size; + uint8_t apiflags; + err_t err; +} ssl_tcp_api_call_t; + +static err_t _tcp_ssl_write_api(struct tcpip_api_call_data *api_call_msg) { + ssl_tcp_api_call_t *msg = (ssl_tcp_api_call_t *)api_call_msg; + msg->err = tcp_write(msg->pcb, msg->data, msg->size, msg->apiflags); + return msg->err; +} + +static err_t _tcp_ssl_output_api(struct tcpip_api_call_data *api_call_msg) { + ssl_tcp_api_call_t *msg = (ssl_tcp_api_call_t *)api_call_msg; + msg->err = tcp_output(msg->pcb); + return msg->err; +} + +static err_t _tcp_ssl_write(tcp_pcb *pcb, const void *data, size_t size, uint8_t apiflags) { + if (!pcb) return ERR_CONN; + ssl_tcp_api_call_t msg; + msg.pcb = pcb; + msg.data = data; + msg.size = size; + msg.apiflags = apiflags; + msg.err = ERR_CONN; + if (tcpip_api_call(_tcp_ssl_write_api, (struct tcpip_api_call_data *)&msg) != ERR_OK) { + return ERR_CONN; + } + return msg.err; +} + +static err_t _tcp_ssl_output(tcp_pcb *pcb) { + if (!pcb) return ERR_CONN; + ssl_tcp_api_call_t msg; + msg.pcb = pcb; + msg.data = NULL; + msg.size = 0; + msg.apiflags = 0; + msg.err = ERR_CONN; + if (tcpip_api_call(_tcp_ssl_output_api, (struct tcpip_api_call_data *)&msg) != ERR_OK) { + return ERR_CONN; + } + return msg.err; +} + /* * Custom LwIP BIO callbacks for mbedTLS * These bridge mbedTLS's I/O with LwIP raw TCP (tcp_pcb). - * The void* ctx points to the AsyncTCP_TLS_Context instance. + * The void* ctx points to the AsyncTCPTLS instance. */ static int _lwip_ssl_send(void *ctx, const unsigned char *buf, size_t len) { - AsyncTCP_TLS_Context *sslctx = (AsyncTCP_TLS_Context *)ctx; + AsyncTCPTLS *sslctx = (AsyncTCPTLS *)ctx; if (!sslctx || !sslctx->pcb()) { return MBEDTLS_ERR_NET_SEND_FAILED; } tcp_pcb *pcb = sslctx->pcb(); - err_t err = tcp_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); + err_t err = _tcp_ssl_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); if (err == ERR_OK) { - tcp_output(pcb); + sslctx->setOutputPending(true); return (int)len; } if (err == ERR_MEM) { @@ -73,7 +135,7 @@ static int _lwip_ssl_send(void *ctx, const unsigned char *buf, size_t len) { } static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { - AsyncTCP_TLS_Context *sslctx = (AsyncTCP_TLS_Context *)ctx; + AsyncTCPTLS *sslctx = (AsyncTCPTLS *)ctx; if (!sslctx || !sslctx->hasRxData()) { return MBEDTLS_ERR_SSL_WANT_READ; } @@ -81,10 +143,10 @@ static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { } /* - * AsyncTCP_TLS_Context implementation + * AsyncTCPTLS implementation */ -AsyncTCP_TLS_Context::AsyncTCP_TLS_Context(void) { +AsyncTCPTLS::AsyncTCPTLS(void) { mbedtls_ssl_init(&ssl_ctx); mbedtls_ssl_config_init(&ssl_conf); mbedtls_ctr_drbg_init(&drbg_ctx); @@ -105,18 +167,24 @@ AsyncTCP_TLS_Context::AsyncTCP_TLS_Context(void) { _ssl_tx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_TX_BUF_SIZE); _ssl_tx_buf_len = 0; _ssl_tx_pos = 0; + _output_pending = false; } -AsyncTCP_TLS_Context::~AsyncTCP_TLS_Context() { +AsyncTCPTLS::~AsyncTCPTLS() { _deleteHandshakeCerts(); - async_tcp_log_v("~AsyncTCP_TLS_Context"); + async_tcp_log_v("~AsyncTCPTLS"); mbedtls_ssl_free(&ssl_ctx); mbedtls_ssl_config_free(&ssl_conf); mbedtls_ctr_drbg_free(&drbg_ctx); mbedtls_entropy_free(&entropy_ctx); + if (_ssl_key_password) { + free(_ssl_key_password); + _ssl_key_password = NULL; + } + if (_ssl_rx_buf) { free(_ssl_rx_buf); _ssl_rx_buf = NULL; @@ -127,7 +195,7 @@ AsyncTCP_TLS_Context::~AsyncTCP_TLS_Context() { } } -void AsyncTCP_TLS_Context::feedRxData(const unsigned char *data, size_t len) { +void AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { if (!_ssl_rx_buf || len == 0) return; size_t space = ASYNCTCP_TLS_RX_BUF_SIZE - _ssl_rx_buf_len; if (len > space) len = space; @@ -136,14 +204,14 @@ void AsyncTCP_TLS_Context::feedRxData(const unsigned char *data, size_t len) { _ssl_rx_total += len; } -size_t AsyncTCP_TLS_Context::flushTxData(void) { +size_t AsyncTCPTLS::flushTxData(void) { if (!_ssl_tx_buf || _ssl_tx_buf_len == 0 || !_pcb) return 0; size_t sent = 0; size_t remaining = _ssl_tx_buf_len - _ssl_tx_pos; if (remaining > 0) { - err_t err = tcp_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, remaining, TCP_WRITE_FLAG_COPY); + err_t err = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, remaining, TCP_WRITE_FLAG_COPY); if (err == ERR_OK) { - tcp_output(_pcb); + _tcp_ssl_output(_pcb); sent = remaining; _ssl_tx_buf_len = 0; _ssl_tx_pos = 0; @@ -151,9 +219,9 @@ size_t AsyncTCP_TLS_Context::flushTxData(void) { // TCP buffer full, try to send what we can size_t space = tcp_sndbuf(_pcb); if (space > 0) { - err_t err2 = tcp_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, space, TCP_WRITE_FLAG_COPY); + err_t err2 = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, space, TCP_WRITE_FLAG_COPY); if (err2 == ERR_OK) { - tcp_output(_pcb); + _tcp_ssl_output(_pcb); sent = space; _ssl_tx_pos += space; } @@ -167,7 +235,7 @@ size_t AsyncTCP_TLS_Context::flushTxData(void) { return sent; } -int AsyncTCP_TLS_Context::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { +int AsyncTCPTLS::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { return _startSSLClient(pcb, host_or_ip, NULL, 0, NULL, 0, @@ -176,7 +244,7 @@ int AsyncTCP_TLS_Context::startSSLClientInsecure(tcp_pcb *pcb, const char *host_ true); } -int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, +int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *pskIdent, const char *psKey) { return _startSSLClient(pcb, host_or_ip, NULL, 0, @@ -186,7 +254,7 @@ int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, false); } -int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, +int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *rootCABuff, const char *cli_cert, const char *cli_key) { @@ -196,7 +264,7 @@ int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, (const unsigned char *)cli_key, (cli_key != NULL) ? strlen(cli_key) + 1 : 0); } -int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, +int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, const unsigned char *cli_key, const size_t cli_key_len) { @@ -208,7 +276,7 @@ int AsyncTCP_TLS_Context::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, false); } -int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, +int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, const unsigned char *cli_key, const size_t cli_key_len, @@ -225,7 +293,6 @@ int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, } async_tcp_log_v("Seeding the random number generator"); - mbedtls_entropy_init(&entropy_ctx); ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, &entropy_ctx, (const unsigned char *)pers, strlen(pers)); @@ -280,6 +347,7 @@ int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, } ret = mbedtls_ssl_conf_psk(&ssl_conf, psk, psk_len, (const unsigned char *)pskIdent, strlen(pskIdent)); + mbedtls_platform_zeroize(psk, sizeof(psk)); if (ret != 0) { async_tcp_log_e("mbedtls_ssl_conf_psk returned %d", ret); return handle_error(ret); @@ -312,6 +380,11 @@ int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, } async_tcp_log_v("Setting hostname for TLS session..."); + if (!host_or_ip || strlen(host_or_ip) > 253) { + _deleteHandshakeCerts(); + async_tcp_log_e("Invalid hostname (too long or NULL)"); + return -1; + } if ((ret = mbedtls_ssl_set_hostname(&ssl_ctx, host_or_ip)) != 0) { _deleteHandshakeCerts(); return handle_error(ret); @@ -335,7 +408,7 @@ int AsyncTCP_TLS_Context::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return 0; } -int AsyncTCP_TLS_Context::startSSLServer(tcp_pcb *pcb, +int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, const unsigned char *server_cert, size_t server_cert_len, const unsigned char *server_key, size_t server_key_len, const char *password) { @@ -345,10 +418,10 @@ int AsyncTCP_TLS_Context::startSSLServer(tcp_pcb *pcb, return -1; } - _ssl_key_password = password; + if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + _ssl_key_password = password ? strdup(password) : NULL; async_tcp_log_v("Seeding the random number generator (server)"); - mbedtls_entropy_init(&entropy_ctx); ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, &entropy_ctx, (const unsigned char *)pers, strlen(pers)); @@ -473,17 +546,19 @@ int AsyncTCP_TLS_Context::startSSLServer(tcp_pcb *pcb, _pcb = pcb; mbedtls_ssl_set_bio(&ssl_ctx, this, _lwip_ssl_send, _lwip_ssl_recv, NULL); handshake_start_time = 0; + handshake_timeout = SSL_HANDSHAKE_TIMEOUT; return 0; } -int AsyncTCP_TLS_Context::runSSLHandshake(void) { +int AsyncTCPTLS::runSSLHandshake(void) { int ret, flags; if (!_pcb) return -1; if (handshake_start_time == 0) handshake_start_time = millis(); ret = mbedtls_ssl_handshake(&ssl_ctx); + flushOutput(); if (ret != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { return handle_error(ret); @@ -527,17 +602,18 @@ int AsyncTCP_TLS_Context::runSSLHandshake(void) { return 0; } -int AsyncTCP_TLS_Context::write(const uint8_t *data, size_t len) { +int AsyncTCPTLS::write(const uint8_t *data, size_t len) { if (!_pcb) return -1; int ret = mbedtls_ssl_write(&ssl_ctx, data, len); + flushOutput(); if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret < 0) { return handle_error(ret); } return ret; } -int AsyncTCP_TLS_Context::read(uint8_t *data, size_t len) { +int AsyncTCPTLS::read(uint8_t *data, size_t len) { if (!_ssl_rx_buf || _ssl_rx_pos >= _ssl_rx_buf_len) return 0; size_t avail = _ssl_rx_buf_len - _ssl_rx_pos; size_t copy = (avail < len) ? avail : len; @@ -552,7 +628,35 @@ int AsyncTCP_TLS_Context::read(uint8_t *data, size_t len) { return (int)copy; } -void AsyncTCP_TLS_Context::_deleteHandshakeCerts(void) { +int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { + if (!_pcb) return -1; + int ret = mbedtls_ssl_read(&ssl_ctx, data, len); + flushOutput(); + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + return 0; + } + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { + async_tcp_log_i("SSL peer close notify"); + return 0; + } + if (ret == MBEDTLS_ERR_SSL_CONN_EOF) { + return 0; + } + if (ret < 0) { + async_tcp_log_e("mbedtls_ssl_read failed: -0x%04x", -ret); + return -1; + } + return ret; +} + +void AsyncTCPTLS::flushOutput(void) { + if (_output_pending && _pcb) { + _tcp_ssl_output(_pcb); + _output_pending = false; + } +} + +void AsyncTCPTLS::_deleteHandshakeCerts(void) { if (_have_ca_cert) { async_tcp_log_v("Cleaning CA certificate."); mbedtls_x509_crt_free(&ca_cert); diff --git a/src/AsyncTCP_TLS_Context.h b/src/AsyncTCPTLS.h similarity index 84% rename from src/AsyncTCP_TLS_Context.h rename to src/AsyncTCPTLS.h index 6b4eb09..1f981fd 100644 --- a/src/AsyncTCP_TLS_Context.h +++ b/src/AsyncTCPTLS.h @@ -20,7 +20,7 @@ struct tcp_pcb; #define ASYNCTCP_TLS_RX_BUF_SIZE 4096 #define ASYNCTCP_TLS_TX_BUF_SIZE 4096 -class AsyncTCP_TLS_Context +class AsyncTCPTLS { private: mbedtls_ssl_context ssl_ctx; @@ -40,8 +40,8 @@ class AsyncTCP_TLS_Context tcp_pcb *_pcb; - // PEM password for encrypted private keys - const char *_ssl_key_password; + // PEM password for encrypted private keys (owned copy via strdup) + char *_ssl_key_password; // Per-connection encrypted data buffers for BIO callbacks unsigned char *_ssl_rx_buf; @@ -53,6 +53,8 @@ class AsyncTCP_TLS_Context size_t _ssl_tx_buf_len; size_t _ssl_tx_pos; + bool _output_pending; // tcp_write queued, tcp_output deferred + int _startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, @@ -61,10 +63,11 @@ class AsyncTCP_TLS_Context bool insecure); void _deleteHandshakeCerts(void); + static void _clear_DER_cache(void); public: - AsyncTCP_TLS_Context(void); - virtual ~AsyncTCP_TLS_Context(); + AsyncTCPTLS(void); + virtual ~AsyncTCPTLS(); // Feed encrypted data from TCP into BIO buffer void feedRxData(const unsigned char *data, size_t len); @@ -77,6 +80,7 @@ class AsyncTCP_TLS_Context // Public accessor for PCB (needed by BIO callbacks) tcp_pcb *pcb() const { return _pcb; } + void setOutputPending(bool v) { _output_pending = v; } int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); @@ -103,6 +107,12 @@ class AsyncTCP_TLS_Context int write(const uint8_t *data, size_t len); int read(uint8_t *data, size_t len); + + // Decrypt application data via mbedtls_ssl_read (BIO pulls encrypted bytes internally) + int sslRead(uint8_t *data, size_t len); + + // Flush deferred tcp_output (called after mbedtls_ssl_handshake/write/read completes) + void flushOutput(void); }; #endif // ASYNC_TCP_SSL_ENABLED diff --git a/src/AsyncTCPVersion.h b/src/AsyncTCPVersion.h index 4dc4073..5fdbea9 100644 --- a/src/AsyncTCPVersion.h +++ b/src/AsyncTCPVersion.h @@ -10,7 +10,7 @@ extern "C" { /** Major version number (X.x.x) */ #define ASYNCTCP_VERSION_MAJOR 3 /** Minor version number (x.X.x) */ -#define ASYNCTCP_VERSION_MINOR 5 +#define ASYNCTCP_VERSION_MINOR 6 /** Patch version number (x.x.X) */ #define ASYNCTCP_VERSION_PATCH 0 From 9c1c6d72a21b5407532093a8e15758f8f7fbdcfb Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 07:45:28 -0700 Subject: [PATCH 03/21] Clear all deferred SSL parameters on close --- src/AsyncTCP.cpp | 14 ++++++++++++++ src/AsyncTCP.h | 1 + 2 files changed, 15 insertions(+) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 50787d5..907516c 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -959,6 +959,16 @@ bool AsyncClient::connect(const char *host, uint16_t port) { } #if ASYNC_TCP_SSL_ENABLED +void AsyncClient::_clearSSLParams(void) { + _ssl_host = ""; + _ssl_ca_cert = NULL; + _ssl_ca_cert_len = 0; + _ssl_client_cert = NULL; + _ssl_client_cert_len = 0; + _ssl_client_key = NULL; + _ssl_client_key_len = 0; +} + bool AsyncClient::beginSecure(const char *host, uint16_t port, const char *rootCA, const char *clientCert, const char *clientKey) { return beginSecure(host, port, @@ -1122,6 +1132,7 @@ int8_t AsyncClient::_close() { _ssl_ctx = 0; _ssl_handshake_done = false; } + _clearSSLParams(); #endif int8_t err = _tcp_close(&_pcb, this); // _pcb is now NULL @@ -1166,6 +1177,7 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { async_tcp_log_e("startSSLClient failed: %d", ret); delete _ssl_ctx; _ssl_ctx = 0; + _clearSSLParams(); if (_error_cb) { _error_cb(_error_cb_arg, this, -60); } @@ -1181,6 +1193,7 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { if (ret != 0) { if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { async_tcp_log_e("SSL handshake failed: %d", ret); + _clearSSLParams(); if (_error_cb) { _error_cb(_error_cb_arg, this, -60); } @@ -1273,6 +1286,7 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { // Still in progress, wait for more data } else { async_tcp_log_e("SSL handshake failed in _recv: %d", ret); + _clearSSLParams(); if (_error_cb) { _error_cb(_error_cb_arg, this, -60); } diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index e6a7741..bcf3c56 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -354,6 +354,7 @@ class AsyncClient { size_t _ssl_client_cert_len; const unsigned char *_ssl_client_key; size_t _ssl_client_key_len; + void _clearSSLParams(void); #endif }; From 9f9bfb25d7ead596923c6e29def95a0c7d056337 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 07:51:49 -0700 Subject: [PATCH 04/21] fix TCP backpressure in the SSL path --- src/AsyncTCP.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 907516c..09c7882 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1312,16 +1312,12 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { pbuf_free(b); } // Decrypt all available plaintext - _ack_pcb = true; uint8_t buf[1024]; int n; while ((n = _ssl_ctx->sslRead(buf, sizeof(buf))) > 0) { if (_recv_cb) { async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); } - if (!_ack_pcb) { - _rx_ack_len += n; - } } _ssl_ctx->flushOutput(); return ERR_OK; From a1e6466c420d6a16e0f8150983d35bddede55b15 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 07:56:26 -0700 Subject: [PATCH 05/21] fix plaintext downgrade --- src/AsyncTCP.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 09c7882..97f1c5d 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1925,9 +1925,13 @@ int8_t AsyncServer::_accepted(AsyncClient *client) { } else { async_tcp_log_e("startSSLServer failed: %d", ret); delete ssl; + client->abort(); + return ERR_ABRT; } } else { async_tcp_log_e("Failed to allocate SSL context for server"); + client->abort(); + return ERR_ABRT; } } #endif From 5adc05ac0c6364fe2b342304f508fae7e6b235cb Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 08:24:25 -0700 Subject: [PATCH 06/21] allow for PEM files to loaded from LittleFS (onSslFileRequest) --- src/AsyncTCP.cpp | 51 +++++++++++++++++++++++++++++++++++++---------- src/AsyncTCP.h | 2 +- src/AsyncTCPTLS.h | 3 ++- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 97f1c5d..2257054 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1867,6 +1867,9 @@ void AsyncServer::end() { } _pcb = NULL; } +#if ASYNC_TCP_SSL_ENABLED + AsyncTCPTLS::_clear_DER_cache(); +#endif } // runs on LwIP thread @@ -1915,22 +1918,50 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { int8_t AsyncServer::_accepted(AsyncClient *client) { #if ASYNC_TCP_SSL_ENABLED - if (_use_ssl && _cert && _key && client && client->pcb()) { - AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); - if (ssl) { - int ret = ssl->startSSLServer(client->pcb(), _cert, _cert_len, _key, _key_len, _ssl_key_password); - if (ret == 0) { - client->_ssl_ctx = ssl; - async_tcp_log_d("Server SSL context ready, handshake will start on first poll"); + if (_use_ssl && client && client->pcb()) { + const unsigned char *cert = _cert; + size_t cert_len = _cert_len; + const unsigned char *key = _key; + size_t key_len = _key_len; + uint8_t *cb_cert = NULL; + uint8_t *cb_key = NULL; + + // If cert/key not pre-loaded, try the file handler callback + if (!cert && !key && _ssl_file_cb) { + int r = _ssl_file_cb(_ssl_file_cb_arg, &cb_cert, &cb_key); + if (r == 0) { + if (cb_cert) { cert = cb_cert; cert_len = strlen((const char *)cb_cert) + 1; } + if (cb_key) { key = cb_key; key_len = strlen((const char *)cb_key) + 1; } + } + } + + if (cert && key) { + AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); + if (ssl) { + int ret = ssl->startSSLServer(client->pcb(), cert, cert_len, key, key_len, _ssl_key_password); + if (ret == 0) { + client->_ssl_ctx = ssl; + async_tcp_log_d("Server SSL context ready, handshake will start on first poll"); + } else { + async_tcp_log_e("startSSLServer failed: %d", ret); + delete ssl; + client->abort(); + if (cb_cert) free(cb_cert); + if (cb_key) free(cb_key); + return ERR_ABRT; + } } else { - async_tcp_log_e("startSSLServer failed: %d", ret); - delete ssl; + async_tcp_log_e("Failed to allocate SSL context for server"); client->abort(); + if (cb_cert) free(cb_cert); + if (cb_key) free(cb_key); return ERR_ABRT; } } else { - async_tcp_log_e("Failed to allocate SSL context for server"); + async_tcp_log_e("SSL enabled but no certificate available"); client->abort(); + if (cb_cert) free(cb_cert); + if (cb_key) free(cb_key); return ERR_ABRT; } } diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index bcf3c56..644033e 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -24,7 +24,7 @@ #include #if ASYNC_TCP_SSL_ENABLED -typedef std::function AcSSlFileHandler; +typedef std::function AcSSlFileHandler; #endif #ifndef LIBRETINY diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 1f981fd..db01aa9 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -63,12 +63,13 @@ class AsyncTCPTLS bool insecure); void _deleteHandshakeCerts(void); - static void _clear_DER_cache(void); public: AsyncTCPTLS(void); virtual ~AsyncTCPTLS(); + static void _clear_DER_cache(void); + // Feed encrypted data from TCP into BIO buffer void feedRxData(const unsigned char *data, size_t len); From f5c390f57877d806f86f61b1903945d08c869463 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 11:24:23 -0700 Subject: [PATCH 07/21] version bump for ssl feature --- library.json | 2 +- library.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library.json b/library.json index 8d07af7..3e5467e 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "AsyncTCP", - "version": "3.5.0", + "version": "3.6.0", "description": "Asynchronous TCP Library for ESP32", "keywords": "async,tcp", "repository": { diff --git a/library.properties b/library.properties index c54f294..47625ab 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=Async TCP includes=AsyncTCP.h -version=3.5.0 +version=3.6.0 author=ESP32Async maintainer=ESP32Async sentence=Async TCP Library for ESP32 From bdc5337f35cfd4d2248c47c232f995129babe668 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 11:34:21 -0700 Subject: [PATCH 08/21] _ssl_rx_buf juggling and move mbedtls to static (saves heap) --- src/AsyncTCP.cpp | 137 +++++++++++++++++++++++++++----------------- src/AsyncTCP.h | 3 +- src/AsyncTCPTLS.cpp | 112 ++++++++++++++++++++---------------- src/AsyncTCPTLS.h | 22 ++++--- 4 files changed, 165 insertions(+), 109 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 2257054..d9f3cdf 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -657,8 +657,9 @@ static err_t _tcp_close_api(struct tcpip_api_call_data *api_call_msg) { tcp_pcb *pcb = *msg->pcb; _reset_tcp_callbacks(pcb, msg->close); if (tcp_close(pcb) != ERR_OK) { - // We do not permit failure here: abandon the pcb anyways. - tcp_abort(pcb); + // tcp_close fails when unsent data remains (e.g. HTTP response not yet ACKed). + // Send FIN gracefully instead of RST to avoid NS_ERROR_NET_RESET. + tcp_shutdown(pcb, 0, 1); } msg->err = ERR_OK; *msg->pcb = nullptr; // PCB is now the property of LwIP @@ -791,6 +792,7 @@ AsyncClient::AsyncClient(tcp_pcb *pcb) _ssl_client_cert_len = 0; _ssl_client_key = 0; _ssl_client_key_len = 0; + _ssl_pending_pbufs = NULL; #endif if (_pcb) { _rx_last_packet = millis(); @@ -800,6 +802,10 @@ AsyncClient::AsyncClient(tcp_pcb *pcb) AsyncClient::~AsyncClient() { #if ASYNC_TCP_SSL_ENABLED + if (_ssl_pending_pbufs) { + pbuf_free(_ssl_pending_pbufs); + _ssl_pending_pbufs = NULL; + } if (_ssl_ctx) { delete _ssl_ctx; _ssl_ctx = 0; @@ -1127,7 +1133,14 @@ void AsyncClient::ackPacket(struct pbuf *pb) { int8_t AsyncClient::_close() { // ets_printf("X: 0x%08x\n", (uint32_t)this); #if ASYNC_TCP_SSL_ENABLED + if (_ssl_pending_pbufs) { + pbuf_free(_ssl_pending_pbufs); + _ssl_pending_pbufs = NULL; + } if (_ssl_ctx) { + // Do NOT call sendCloseNotify() here — it queues data via tcp_write(), + // which causes tcp_close() to always fail (unsent data), forcing the + // fallback to tcp_shutdown() that leaks PCBs in FIN_WAIT/LAST_ACK. delete _ssl_ctx; _ssl_ctx = 0; _ssl_handshake_done = false; @@ -1241,6 +1254,10 @@ int8_t AsyncClient::_lwip_fin(tcp_pcb *pcb, int8_t err) { // In Async Thread int8_t AsyncClient::_fin(tcp_pcb *pcb, int8_t err) { #if ASYNC_TCP_SSL_ENABLED + if (_ssl_pending_pbufs) { + pbuf_free(_ssl_pending_pbufs); + _ssl_pending_pbufs = NULL; + } if (_ssl_ctx) { delete _ssl_ctx; _ssl_ctx = 0; @@ -1262,18 +1279,26 @@ int8_t AsyncClient::_sent(tcp_pcb *pcb, uint16_t len) { int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { #if ASYNC_TCP_SSL_ENABLED if (_ssl_ctx && !_ssl_handshake_done) { - // During handshake: feed all received data into SSL rx buffer + // During handshake: buffer encrypted data, ack full pbuf size to TCP + // (LwIP requires exact ack — pbufs already removed from receive queue) + size_t total_recved = 0; while (pb != NULL) { _rx_last_packet = millis(); + if (!_ssl_ctx->feedRxData((const unsigned char *)pb->payload, pb->len)) { + // BIO buffer full — hold remaining pbufs without acking + // LwIP backpressures naturally via TCP window + _ssl_pending_pbufs = pb; + break; + } pbuf *b = pb; pb = b->next; b->next = NULL; - _ssl_ctx->feedRxData((const unsigned char *)b->payload, b->len); - if (_pcb) { - _tcp_recved(&_pcb, b->len); - } + total_recved += b->len; pbuf_free(b); } + if (total_recved > 0 && _pcb) { + _tcp_recved(&_pcb, total_recved); + } // Try to continue handshake int ret = _ssl_ctx->runSSLHandshake(); if (ret == 0) { @@ -1298,19 +1323,24 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { } if (_ssl_ctx && _ssl_handshake_done) { - // SSL established: feed encrypted data, decrypt, deliver plaintext + // SSL established: buffer encrypted data, ack full pbuf size to TCP + size_t total_recved = 0; while (pb != NULL) { _rx_last_packet = millis(); + if (!_ssl_ctx->feedRxData((const unsigned char *)pb->payload, pb->len)) { + // BIO buffer full — hold remaining pbufs without acking + _ssl_pending_pbufs = pb; + break; + } pbuf *b = pb; pb = b->next; b->next = NULL; - _ssl_ctx->feedRxData((const unsigned char *)b->payload, b->len); - // Ack data to TCP immediately — it's now buffered in the SSL context - if (_pcb) { - _tcp_recved(&_pcb, b->len); - } + total_recved += b->len; pbuf_free(b); } + if (total_recved > 0 && _pcb) { + _tcp_recved(&_pcb, total_recved); + } // Decrypt all available plaintext uint8_t buf[1024]; int n; @@ -1388,6 +1418,39 @@ int8_t AsyncClient::_poll(tcp_pcb *pcb) { } return ERR_OK; } + + // Process pending SSL pbufs — drain BIO buffer first to make room + if (_ssl_pending_pbufs && _ssl_ctx && _ssl_handshake_done) { + uint8_t buf[1024]; + int n; + while ((n = _ssl_ctx->sslRead(buf, sizeof(buf))) > 0) { + if (_recv_cb) { + async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); + } + } + _ssl_ctx->flushOutput(); + + // Try to feed held pbufs now that BIO buffer has drained + pbuf *pb = _ssl_pending_pbufs; + _ssl_pending_pbufs = NULL; + size_t total_recved = 0; + while (pb != NULL) { + _rx_last_packet = millis(); + if (!_ssl_ctx->feedRxData((const unsigned char *)pb->payload, pb->len)) { + // Still no room — save rest for next poll + _ssl_pending_pbufs = pb; + break; + } + pbuf *b = pb; + pb = b->next; + b->next = NULL; + total_recved += b->len; + pbuf_free(b); + } + if (total_recved > 0 && _pcb) { + _tcp_recved(&_pcb, total_recved); + } + } #endif // ACK Timeout @@ -1918,50 +1981,22 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { int8_t AsyncServer::_accepted(AsyncClient *client) { #if ASYNC_TCP_SSL_ENABLED - if (_use_ssl && client && client->pcb()) { - const unsigned char *cert = _cert; - size_t cert_len = _cert_len; - const unsigned char *key = _key; - size_t key_len = _key_len; - uint8_t *cb_cert = NULL; - uint8_t *cb_key = NULL; - - // If cert/key not pre-loaded, try the file handler callback - if (!cert && !key && _ssl_file_cb) { - int r = _ssl_file_cb(_ssl_file_cb_arg, &cb_cert, &cb_key); - if (r == 0) { - if (cb_cert) { cert = cb_cert; cert_len = strlen((const char *)cb_cert) + 1; } - if (cb_key) { key = cb_key; key_len = strlen((const char *)cb_key) + 1; } - } - } - - if (cert && key) { - AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); - if (ssl) { - int ret = ssl->startSSLServer(client->pcb(), cert, cert_len, key, key_len, _ssl_key_password); - if (ret == 0) { - client->_ssl_ctx = ssl; - async_tcp_log_d("Server SSL context ready, handshake will start on first poll"); - } else { - async_tcp_log_e("startSSLServer failed: %d", ret); - delete ssl; - client->abort(); - if (cb_cert) free(cb_cert); - if (cb_key) free(cb_key); - return ERR_ABRT; - } + if (_use_ssl && _cert && _key && client && client->pcb()) { + AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); + if (ssl) { + int ret = ssl->startSSLServer(client->pcb(), _cert, _cert_len, _key, _key_len, _ssl_key_password); + if (ret == 0) { + client->_ssl_ctx = ssl; + async_tcp_log_d("Server SSL context ready, handshake will start on first poll"); } else { - async_tcp_log_e("Failed to allocate SSL context for server"); + async_tcp_log_e("startSSLServer failed: %d", ret); + delete ssl; client->abort(); - if (cb_cert) free(cb_cert); - if (cb_key) free(cb_key); return ERR_ABRT; } } else { - async_tcp_log_e("SSL enabled but no certificate available"); + async_tcp_log_e("Failed to allocate SSL context for server"); client->abort(); - if (cb_cert) free(cb_cert); - if (cb_key) free(cb_key); return ERR_ABRT; } } diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 644033e..c3fecbd 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -284,7 +284,7 @@ class AsyncClient { #if ASYNC_TCP_SSL_ENABLED // SSL support - bool beginSecure(const char *host, uint16_t port, const char *rootCA = NULL, + bool beginSecure(const char *host, uint16_t port, const char *rootCA, const char *clientCert = NULL, const char *clientKey = NULL); bool beginSecure(const char *host, uint16_t port, const unsigned char *rootCA, size_t rootCALen, @@ -355,6 +355,7 @@ class AsyncClient { const unsigned char *_ssl_client_key; size_t _ssl_client_key_len; void _clearSSLParams(void); + pbuf *_ssl_pending_pbufs; #endif }; diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 148daa0..9542d68 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -19,7 +19,6 @@ extern "C" { # warning "Please configure IDF framework to include mbedTLS -> Enable pre-shared-key ciphersuites and activate at least one cipher" #else -static const char *pers = "esp32-tls"; // From mbedtls/net_sockets.h — not included since we use custom LwIP BIO #ifndef MBEDTLS_ERR_NET_SEND_FAILED @@ -47,6 +46,10 @@ static int _handle_error(int err) { if (err == -30848) { return err; } + // Suppress connection reset — normal client disconnect + if (err == -0x004E) { + return err; + } #ifdef MBEDTLS_ERROR_C char error_buf[100]; mbedtls_strerror(err, error_buf, 100); @@ -73,12 +76,9 @@ typedef struct { static err_t _tcp_ssl_write_api(struct tcpip_api_call_data *api_call_msg) { ssl_tcp_api_call_t *msg = (ssl_tcp_api_call_t *)api_call_msg; msg->err = tcp_write(msg->pcb, msg->data, msg->size, msg->apiflags); - return msg->err; -} - -static err_t _tcp_ssl_output_api(struct tcpip_api_call_data *api_call_msg) { - ssl_tcp_api_call_t *msg = (ssl_tcp_api_call_t *)api_call_msg; - msg->err = tcp_output(msg->pcb); + if (msg->err == ERR_OK) { + msg->err = tcp_output(msg->pcb); + } return msg->err; } @@ -96,20 +96,6 @@ static err_t _tcp_ssl_write(tcp_pcb *pcb, const void *data, size_t size, uint8_t return msg.err; } -static err_t _tcp_ssl_output(tcp_pcb *pcb) { - if (!pcb) return ERR_CONN; - ssl_tcp_api_call_t msg; - msg.pcb = pcb; - msg.data = NULL; - msg.size = 0; - msg.apiflags = 0; - msg.err = ERR_CONN; - if (tcpip_api_call(_tcp_ssl_output_api, (struct tcpip_api_call_data *)&msg) != ERR_OK) { - return ERR_CONN; - } - return msg.err; -} - /* * Custom LwIP BIO callbacks for mbedTLS * These bridge mbedTLS's I/O with LwIP raw TCP (tcp_pcb). @@ -125,7 +111,6 @@ static int _lwip_ssl_send(void *ctx, const unsigned char *buf, size_t len) { err_t err = _tcp_ssl_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); if (err == ERR_OK) { - sslctx->setOutputPending(true); return (int)len; } if (err == ERR_MEM) { @@ -146,11 +131,24 @@ static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { * AsyncTCPTLS implementation */ +// Static shared RNG — initialized once, serialized on async task +mbedtls_ctr_drbg_context AsyncTCPTLS::drbg_ctx; +mbedtls_entropy_context AsyncTCPTLS::entropy_ctx; +bool AsyncTCPTLS::_conf_initialized = false; + +void AsyncTCPTLS::_init_rng(void) { + if (_conf_initialized) return; + mbedtls_ctr_drbg_init(&drbg_ctx); + mbedtls_entropy_init(&entropy_ctx); + mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, + &entropy_ctx, (const unsigned char *)"AsyncTCPTLS", 11); + _conf_initialized = true; +} + AsyncTCPTLS::AsyncTCPTLS(void) { mbedtls_ssl_init(&ssl_ctx); mbedtls_ssl_config_init(&ssl_conf); - mbedtls_ctr_drbg_init(&drbg_ctx); - mbedtls_entropy_init(&entropy_ctx); + _init_rng(); _pcb = NULL; _ssl_key_password = NULL; _have_ca_cert = false; @@ -160,6 +158,7 @@ AsyncTCPTLS::AsyncTCPTLS(void) { handshake_start_time = 0; _ssl_rx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_RX_BUF_SIZE); + _ssl_rx_buf_capacity = _ssl_rx_buf ? ASYNCTCP_TLS_RX_BUF_SIZE : 0; _ssl_rx_buf_len = 0; _ssl_rx_pos = 0; _ssl_rx_total = 0; @@ -167,7 +166,6 @@ AsyncTCPTLS::AsyncTCPTLS(void) { _ssl_tx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_TX_BUF_SIZE); _ssl_tx_buf_len = 0; _ssl_tx_pos = 0; - _output_pending = false; } AsyncTCPTLS::~AsyncTCPTLS() { @@ -177,8 +175,6 @@ AsyncTCPTLS::~AsyncTCPTLS() { mbedtls_ssl_free(&ssl_ctx); mbedtls_ssl_config_free(&ssl_conf); - mbedtls_ctr_drbg_free(&drbg_ctx); - mbedtls_entropy_free(&entropy_ctx); if (_ssl_key_password) { free(_ssl_key_password); @@ -195,13 +191,36 @@ AsyncTCPTLS::~AsyncTCPTLS() { } } -void AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { - if (!_ssl_rx_buf || len == 0) return; - size_t space = ASYNCTCP_TLS_RX_BUF_SIZE - _ssl_rx_buf_len; - if (len > space) len = space; +bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { + if (!_ssl_rx_buf || len == 0) return true; + + // Compact: move unconsumed data to front to reclaim space + if (_ssl_rx_pos > 0) { + size_t remaining = _ssl_rx_buf_len - _ssl_rx_pos; + if (remaining > 0) { + memmove(_ssl_rx_buf, _ssl_rx_buf + _ssl_rx_pos, remaining); + } + _ssl_rx_buf_len = remaining; + _ssl_rx_pos = 0; + } + + // Check hard cap — if exceeded, caller must hold the pbuf + if (_ssl_rx_buf_len + len > ASYNCTCP_TLS_RX_BUF_MAX) { + return false; + } + + // Grow buffer if needed (up to cap) + if (_ssl_rx_buf_len + len > _ssl_rx_buf_capacity) { + size_t need = _ssl_rx_buf_len + len; + unsigned char *newbuf = (unsigned char *)realloc(_ssl_rx_buf, need); + if (!newbuf) return false; + _ssl_rx_buf = newbuf; + _ssl_rx_buf_capacity = need; + } memcpy(_ssl_rx_buf + _ssl_rx_buf_len, data, len); _ssl_rx_buf_len += len; _ssl_rx_total += len; + return true; } size_t AsyncTCPTLS::flushTxData(void) { @@ -211,7 +230,6 @@ size_t AsyncTCPTLS::flushTxData(void) { if (remaining > 0) { err_t err = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, remaining, TCP_WRITE_FLAG_COPY); if (err == ERR_OK) { - _tcp_ssl_output(_pcb); sent = remaining; _ssl_tx_buf_len = 0; _ssl_tx_pos = 0; @@ -219,9 +237,9 @@ size_t AsyncTCPTLS::flushTxData(void) { // TCP buffer full, try to send what we can size_t space = tcp_sndbuf(_pcb); if (space > 0) { + if (space > remaining) space = remaining; err_t err2 = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, space, TCP_WRITE_FLAG_COPY); if (err2 == ERR_OK) { - _tcp_ssl_output(_pcb); sent = space; _ssl_tx_pos += space; } @@ -292,14 +310,6 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return -1; } - async_tcp_log_v("Seeding the random number generator"); - - ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, - &entropy_ctx, (const unsigned char *)pers, strlen(pers)); - if (ret < 0) { - return handle_error(ret); - } - async_tcp_log_v("Setting up the SSL/TLS structure..."); if ((ret = mbedtls_ssl_config_defaults(&ssl_conf, @@ -423,12 +433,6 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, async_tcp_log_v("Seeding the random number generator (server)"); - ret = mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, - &entropy_ctx, (const unsigned char *)pers, strlen(pers)); - if (ret < 0) { - return handle_error(ret); - } - async_tcp_log_v("Setting up the SSL/TLS structure (server)..."); if ((ret = mbedtls_ssl_config_defaults(&ssl_conf, @@ -650,20 +654,28 @@ int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { } void AsyncTCPTLS::flushOutput(void) { - if (_output_pending && _pcb) { - _tcp_ssl_output(_pcb); - _output_pending = false; + // No-op: tcp_output is now inlined in _tcp_ssl_write +} + +void AsyncTCPTLS::sendCloseNotify(void) { + if (!_pcb) return; + int ret = mbedtls_ssl_close_notify(&ssl_ctx); + flushOutput(); + if (ret != 0) { + async_tcp_log_d("close_notify: %d", ret); } } void AsyncTCPTLS::_deleteHandshakeCerts(void) { if (_have_ca_cert) { async_tcp_log_v("Cleaning CA certificate."); + mbedtls_ssl_conf_ca_chain(&ssl_conf, NULL, NULL); mbedtls_x509_crt_free(&ca_cert); _have_ca_cert = false; } if (_have_client_cert) { async_tcp_log_v("Cleaning client certificate."); + mbedtls_ssl_conf_own_cert(&ssl_conf, NULL, NULL); mbedtls_x509_crt_free(&client_cert); _have_client_cert = false; } diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index db01aa9..85c2ba1 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -18,6 +18,7 @@ struct tcp_pcb; #define ASYNCTCP_TLS_EOF(r) (((r) == MBEDTLS_ERR_SSL_CONN_EOF) || ((r) == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY)) #define ASYNCTCP_TLS_RX_BUF_SIZE 4096 +#define ASYNCTCP_TLS_RX_BUF_MAX 16384 #define ASYNCTCP_TLS_TX_BUF_SIZE 4096 class AsyncTCPTLS @@ -25,8 +26,13 @@ class AsyncTCPTLS private: mbedtls_ssl_context ssl_ctx; mbedtls_ssl_config ssl_conf; - mbedtls_ctr_drbg_context drbg_ctx; - mbedtls_entropy_context entropy_ctx; + + // Shared across all instances — initialized once, RNG is serialized on async task + static mbedtls_ctr_drbg_context drbg_ctx; + static mbedtls_entropy_context entropy_ctx; + static bool _conf_initialized; + + static void _init_rng(void); mbedtls_x509_crt ca_cert; mbedtls_x509_crt client_cert; @@ -45,6 +51,7 @@ class AsyncTCPTLS // Per-connection encrypted data buffers for BIO callbacks unsigned char *_ssl_rx_buf; + size_t _ssl_rx_buf_capacity; // current allocation size (grows via realloc) size_t _ssl_rx_buf_len; size_t _ssl_rx_pos; size_t _ssl_rx_total; // total bytes buffered, for tcp_recved() @@ -53,8 +60,6 @@ class AsyncTCPTLS size_t _ssl_tx_buf_len; size_t _ssl_tx_pos; - bool _output_pending; // tcp_write queued, tcp_output deferred - int _startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, @@ -70,8 +75,9 @@ class AsyncTCPTLS static void _clear_DER_cache(void); - // Feed encrypted data from TCP into BIO buffer - void feedRxData(const unsigned char *data, size_t len); + // Feed encrypted data from TCP into BIO buffer (returns false if buffer full) + bool feedRxData(const unsigned char *data, size_t len); + size_t rxBufLen() const { return _ssl_rx_buf_len - _ssl_rx_pos; } // Flush BIO tx buffer to TCP (returns bytes flushed, 0 if nothing to flush) size_t flushTxData(void); @@ -81,7 +87,6 @@ class AsyncTCPTLS // Public accessor for PCB (needed by BIO callbacks) tcp_pcb *pcb() const { return _pcb; } - void setOutputPending(bool v) { _output_pending = v; } int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); @@ -114,6 +119,9 @@ class AsyncTCPTLS // Flush deferred tcp_output (called after mbedtls_ssl_handshake/write/read completes) void flushOutput(void); + + // Send TLS close_notify alert and flush to TCP + void sendCloseNotify(void); }; #endif // ASYNC_TCP_SSL_ENABLED From 2000aceb802a0e1e5cf4bc7efaf70f21fc376952 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 12:01:33 -0700 Subject: [PATCH 09/21] do not need flushSSLTxData --- src/AsyncTCP.cpp | 9 ++----- src/AsyncTCP.h | 1 - src/AsyncTCPTLS.cpp | 59 ++++++++++++--------------------------------- src/AsyncTCPTLS.h | 12 +++------ 4 files changed, 21 insertions(+), 60 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index d9f3cdf..c9ff13c 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1008,13 +1008,6 @@ void AsyncClient::feedSSLRxData(const unsigned char *data, size_t len) { } } -size_t AsyncClient::flushSSLTxData() { - if (_ssl_ctx) { - return _ssl_ctx->flushTxData(); - } - return 0; -} - bool AsyncClient::hasSSLRxData() const { if (_ssl_ctx) { return _ssl_ctx->hasRxData(); @@ -1206,6 +1199,7 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { if (ret != 0) { if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { async_tcp_log_e("SSL handshake failed: %d", ret); + _ssl_ctx->logBioState("handshake_poll"); _clearSSLParams(); if (_error_cb) { _error_cb(_error_cb_arg, this, -60); @@ -1311,6 +1305,7 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { // Still in progress, wait for more data } else { async_tcp_log_e("SSL handshake failed in _recv: %d", ret); + _ssl_ctx->logBioState("handshake"); _clearSSLParams(); if (_error_cb) { _error_cb(_error_cb_arg, this, -60); diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index c3fecbd..9ae84ae 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -295,7 +295,6 @@ class AsyncClient { uint32_t getSSLReceiveTimeout() const { return _ssl_timeout; } AsyncTCPTLS *getSSLContext() { return _ssl_ctx; } void feedSSLRxData(const unsigned char *data, size_t len); - size_t flushSSLTxData(); bool hasSSLRxData() const; int sslRead(uint8_t *data, size_t len); int sslWrite(const uint8_t *data, size_t len); diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 9542d68..7c6cbe7 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -161,11 +161,6 @@ AsyncTCPTLS::AsyncTCPTLS(void) { _ssl_rx_buf_capacity = _ssl_rx_buf ? ASYNCTCP_TLS_RX_BUF_SIZE : 0; _ssl_rx_buf_len = 0; _ssl_rx_pos = 0; - _ssl_rx_total = 0; - - _ssl_tx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_TX_BUF_SIZE); - _ssl_tx_buf_len = 0; - _ssl_tx_pos = 0; } AsyncTCPTLS::~AsyncTCPTLS() { @@ -185,10 +180,6 @@ AsyncTCPTLS::~AsyncTCPTLS() { free(_ssl_rx_buf); _ssl_rx_buf = NULL; } - if (_ssl_tx_buf) { - free(_ssl_tx_buf); - _ssl_tx_buf = NULL; - } } bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { @@ -219,40 +210,9 @@ bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { } memcpy(_ssl_rx_buf + _ssl_rx_buf_len, data, len); _ssl_rx_buf_len += len; - _ssl_rx_total += len; return true; } -size_t AsyncTCPTLS::flushTxData(void) { - if (!_ssl_tx_buf || _ssl_tx_buf_len == 0 || !_pcb) return 0; - size_t sent = 0; - size_t remaining = _ssl_tx_buf_len - _ssl_tx_pos; - if (remaining > 0) { - err_t err = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, remaining, TCP_WRITE_FLAG_COPY); - if (err == ERR_OK) { - sent = remaining; - _ssl_tx_buf_len = 0; - _ssl_tx_pos = 0; - } else if (err == ERR_MEM) { - // TCP buffer full, try to send what we can - size_t space = tcp_sndbuf(_pcb); - if (space > 0) { - if (space > remaining) space = remaining; - err_t err2 = _tcp_ssl_write(_pcb, _ssl_tx_buf + _ssl_tx_pos, space, TCP_WRITE_FLAG_COPY); - if (err2 == ERR_OK) { - sent = space; - _ssl_tx_pos += space; - } - } - } - } - if (sent == 0 && _ssl_tx_buf_len > 0 && _ssl_tx_pos >= _ssl_tx_buf_len) { - _ssl_tx_buf_len = 0; - _ssl_tx_pos = 0; - } - return sent; -} - int AsyncTCPTLS::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { return _startSSLClient(pcb, host_or_ip, NULL, 0, @@ -402,8 +362,10 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); - // Reduce buffer sizes to fit ESP32 heap + // Reduce buffer sizes to fit ESP32 heap (requires MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) +#if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) mbedtls_ssl_conf_max_frag_len(&ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); +#endif if ((ret = mbedtls_ssl_setup(&ssl_ctx, &ssl_conf)) != 0) { _deleteHandshakeCerts(); @@ -446,8 +408,10 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, mbedtls_ssl_conf_min_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_max_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); - // Disable renegotiation + // Disable renegotiation (only available if MBEDTLS_SSL_RENEGOTIATION is enabled) +#if defined(MBEDTLS_SSL_RENEGOTIATION) mbedtls_ssl_conf_renegotiation(&ssl_conf, MBEDTLS_SSL_RENEGOTIATION_DISABLED); +#endif // Pin fast cipher suite — hardware-accelerated AES-GCM + SHA256 on ESP32 static const int server_ciphersuites[] = { @@ -540,7 +504,9 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); +#if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) mbedtls_ssl_conf_max_frag_len(&ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); +#endif if ((ret = mbedtls_ssl_setup(&ssl_ctx, &ssl_conf)) != 0) { _deleteHandshakeCerts(); @@ -627,7 +593,6 @@ int AsyncTCPTLS::read(uint8_t *data, size_t len) { if (_ssl_rx_pos >= _ssl_rx_buf_len) { _ssl_rx_buf_len = 0; _ssl_rx_pos = 0; - _ssl_rx_total = 0; } return (int)copy; } @@ -648,6 +613,7 @@ int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { } if (ret < 0) { async_tcp_log_e("mbedtls_ssl_read failed: -0x%04x", -ret); + logBioState("sslRead"); return -1; } return ret; @@ -657,6 +623,13 @@ void AsyncTCPTLS::flushOutput(void) { // No-op: tcp_output is now inlined in _tcp_ssl_write } +void AsyncTCPTLS::logBioState(const char *tag) const { + async_tcp_log_e("%s: rx_buf=%u/%u pos=%u rxBufLen=%u bytes_avail=%d", + tag, (unsigned)_ssl_rx_buf_len, (unsigned)_ssl_rx_buf_capacity, + (unsigned)_ssl_rx_pos, (unsigned)(_ssl_rx_buf_len - _ssl_rx_pos), + (int)mbedtls_ssl_get_bytes_avail((mbedtls_ssl_context *)&ssl_ctx)); +} + void AsyncTCPTLS::sendCloseNotify(void) { if (!_pcb) return; int ret = mbedtls_ssl_close_notify(&ssl_ctx); diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 85c2ba1..75b280f 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -19,7 +19,6 @@ struct tcp_pcb; #define ASYNCTCP_TLS_RX_BUF_SIZE 4096 #define ASYNCTCP_TLS_RX_BUF_MAX 16384 -#define ASYNCTCP_TLS_TX_BUF_SIZE 4096 class AsyncTCPTLS { @@ -54,11 +53,6 @@ class AsyncTCPTLS size_t _ssl_rx_buf_capacity; // current allocation size (grows via realloc) size_t _ssl_rx_buf_len; size_t _ssl_rx_pos; - size_t _ssl_rx_total; // total bytes buffered, for tcp_recved() - - unsigned char *_ssl_tx_buf; - size_t _ssl_tx_buf_len; - size_t _ssl_tx_pos; int _startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, @@ -79,15 +73,15 @@ class AsyncTCPTLS bool feedRxData(const unsigned char *data, size_t len); size_t rxBufLen() const { return _ssl_rx_buf_len - _ssl_rx_pos; } - // Flush BIO tx buffer to TCP (returns bytes flushed, 0 if nothing to flush) - size_t flushTxData(void); - // Check if BIO has buffered rx data available bool hasRxData(void) const { return _ssl_rx_buf && (_ssl_rx_pos < _ssl_rx_buf_len); } // Public accessor for PCB (needed by BIO callbacks) tcp_pcb *pcb() const { return _pcb; } + // Diagnostic: log BIO buffer state + void logBioState(const char *tag) const; + int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, From fd4c3694687c6063ed4616c938a21bb62ba2049f Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 12:08:10 -0700 Subject: [PATCH 10/21] quick example, includes PEM --- .../AsyncWebServerSSL/AsyncWebServerSSL.ino | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino b/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino index a0fbea0..7756567 100644 --- a/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino +++ b/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino @@ -3,11 +3,13 @@ * * ESP32 HTTPS server using ESPAsyncWebServer + AsyncTCPSSL. * Serves a simple page and a JSON API over TLS 1.2 on port 443. + * Includes a ready-to-use self-signed cert (key password: test123). * - * Generate your own self-signed cert/key: - * openssl req -x509 -newkey rsa:2048 -nodes \ + * Regenerate your own: + * openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ * -keyout server.key -out server.pem -days 3650 \ - * -subj "/CN=esp32.local" + * -subj "/CN=esp32.local" \ + * -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" * * Test with: * curl -k https:///api @@ -23,17 +25,63 @@ const char *SSID = "YOUR_SSID"; const char *PASSWORD = "YOUR_PASSWORD"; -// Replace with your own PEM cert and key +// Self-signed cert for esp32.local — key password: test123 +// Regenerate: openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ +// -keyout server.key -out server.pem -days 3650 \ +// -subj "/CN=esp32.local" -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" static const char SERVER_CERT[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- -REPLACE_WITH_YOUR_CERT_PEM +MIIDKzCCAhOgAwIBAgIUdcrSmRQHfj1jGuDmkB1xOVbdt64wDQYJKoZIhvcNAQEL +BQAwFjEUMBIGA1UEAwwLZXNwMzIubG9jYWwwHhcNMjYwODI1MTkwMzU1WhcNMzYw +ODIyMTkwMzU1WjAWMRQwEgYDVQQDDAtlc3AzMi5sb2NhbDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALHDfpBW6MqSH55rLLFuY5eHYi5S/bH3du5Jdw5c +t7qmc55pIbe0zMBCHKzfua8KI/0OWq/kJBynXcda680pRJ5u9AVmqXEqVQxJ6FW/ +vHtRDdBz7rEcGr1rtzmnP6KTxjaXc9AZPQloz18dTyxSwJq1h8sdl7Vtoz5bQzeG +FR2bWZU5z8PUeddVVsX9oUFAC2AfzV5wAC1KH+F8KRmVbGoon4T5T0dE66Sz2DQY +J8BvJUCjTh2E1r+tR8Y3wN9L1sw2InHsPOH1mzWNI2gBoWIqrHL9GSuLfYfUstbn +qA3eoeILluph/3s8XE7oLiEveeo3l1XVI0dZLBRcPcqNmYMCAwEAAaNxMG8wHQYD +VR0OBBYEFABAm1DHsG64G9CdObCOauNH9jKoMB8GA1UdIwQYMBaAFABAm1DHsG64 +G9CdObCOauNH9jKoMA8GA1UdEwEB/wQFMAMBAf8wHAYDVR0RBBUwE4ILZXNwMzIu +bG9jYWyHBMCoBAEwDQYJKoZIhvcNAQELBQADggEBADvl8Gf13zSArF1dCgXFDtPs +xdnbTDT/dtLgOFA85JxPWCizzzAKJElU1ZuufOjOocTM8bxEv7XpicTHaK5RxNAi +77oAMYYeU5HcsU127gMrLpA7H9DU7MulwKfadGw5aa22YD8Fbue50SEx3OE5jHpL +++IMPKF/HoN5GyP5+yhd8HjadnA1BVv3ylAn+ZOH4szcUZ+vRb9OZsoWgjeiph1c +bEP7VWWNxcZSJS/geZP2xvZoSXgOGlzGfFz+MnY3TyIwo6O8W6uYHCQ7us8kEW4V +O6o3be94uiadaVZzhHe8i97oSUqpJHpvXce1r0rP/d7m78/2LDUX+YPYaV+D2X8= -----END CERTIFICATE----- )EOF"; static const char SERVER_KEY[] PROGMEM = R"EOF( ------BEGIN RSA PRIVATE KEY----- -REPLACE_WITH_YOUR_KEY_PEM ------END RSA PRIVATE KEY----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQT1LwhLF3QJtPmEPo +paqR8gICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEBrUrlk3/9b7bU6b +5Z2H7doEggTQiN3qZeQO4fRXqx1TwoAdgOq37uqbkCPf5zXPg9oLNH85S7E9ndfS +x0PnUizxZ/BXsoTf28B5yqXnSMOsaeJr6aIHx9V5is+rVOQfdY8Rmt+xyidmhmC7 +o4FJd3DWzF7aQi/PMJrNY5r3EnAqxxPy3kwjlfo42+ubYXHrB4xS4dHaQv/MoMq1 +Mxb+SjP4/UcO+97zer6wSogDZdxQO+uvt37Y0gMKfpTn2QJnDnuu8WN/b1EKcSFm +jwEoLVIfF8J2i5W03zWCmVMK568R7eEkGDq4/aMnScEN2tYWt3gPk4kNK3qQ1fyN +PbobbtSQzstQ4sEiylZkL9JDHISJu7KNUc6JLZlUyhhmF1eTMWyWzUaXuO+OjRoq +JBdAPSjyJQDLiGIcfI4xuqXDssqTQy9WqKkbXCIBloahDPyM8akW7FgCT8gDw9i+ +DiGJzyVTNfr7X3OWCJ5tuyCpQI4Mkyi5et1p0ESOS+ctfvRKe9yvRmpI0Kvrzs5S +40TncsQJi9WpFPOrE0BgpftMGN1fW2Mq7R43eZ29vMfY4WFf9yeMV73waFcXH621 +dTnf8eDae0NMM3IpGher8hDtpVSdh3Np3VZsrtPRIdcAFjOodwLFgUzk1DXT+tvB +HJOUJwEd3tLHmxo3cuXn09SOY4cuHckka5Cmo9wkQxkCx8lCjdNgGmAa7op5WSvs +MTwij+s1eNk7mMZce+CFHRLU8tcl05fqI/tCEfjRwBa25vKMxG8/g19/HVANaMnL +tGS+ao3YeOxrFNzSYYB6FT16dc/P1g32BSIRnEgEULYFxQDhl/A+oiTDSHXoLl9d +djCvdMi2zX89UZ4sIZWgSYu4UlhQzLiOt7Fa4EO/S4Z5WimR9ag7yDZCtCSWBeUg +7dbGx4tgU4sLvF+hKAVcreHJoBeyWblL1JaY2NbLLJ8yucbV6MuvtQLmgO5pKF27 +WOjPVHlQ6mMCcJ8FX8SuT6F/4ZeKAHOHQRTM85Ai9WBPaCnGlLoIK6DwOU+7LoV3 +smWq4cx7qGUo2Nx7laITFxy4zggqsIxHJ9tckQDWkGHOClT+nrVhsBF59d/i4uen +3REGhW0IpJU/OhZnmbawDMh6nSW3VSQhE4wkC6lr0pFnOtlnYJzpdHibpQoUO8DD +SxzL8AG5Hdz/eORny88/a7kivmEFWQ3L1SX9cqHq5zXvuFWv6bYvKhhrwWUh13HK +MeeoeKqDGF+rt1jznZjcHJmSq5QI2HoaOhhj7QAUai72mi1b8ZtwYHF+ORnp1oi0 +vCw9RKiidHYFQ3ugC5OreBvElzWBjv2Xlf2nHtBhvXFRzTowmEUORMlrWAEJ7ByB +QH72J8GsLR9+NNJiGPJ3OreQ6TsIhmJycyjOlfyz9qK5faz6XnoBzqdGe4iwWd1n +UkT9OrK6lrb1zIKRzVXgxKdopSQ6RKkk7x8SYyTZwLJwSBIx19tZJh7wmRVMNToz +SXOgJAcjB8T1Dz0eaiDT0kwFiaSqwngQXo70x+rKsriRwJ9EXrJ5OQZ61YWj75GJ +ynLof9teDiX4IxOY8brkJqAIkFasjQggJx9PAmEWOodHUinfmy3ppKDEjmmmlngN +CcL72QSXG4Pjb0QUXb8tnDfnrTPcffQbKgTrqvjFpPKDlATFaUeVz+8= +-----END ENCRYPTED PRIVATE KEY----- )EOF"; AsyncWebServer server(443); @@ -88,7 +136,7 @@ void setup() { }); // Start HTTPS server - server.beginSecure(SERVER_CERT, SERVER_KEY, NULL); + server.beginSecure(SERVER_CERT, SERVER_KEY, "test123"); Serial.printf("[Server] HTTPS on port 443\n"); Serial.printf("Test: curl -k https://%s/api\n", WiFi.localIP().toString().c_str()); From 4f091237d8e4bf341b18c9d387c90d3bb7a0fade Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 12:14:13 -0700 Subject: [PATCH 11/21] Revert "version bump for ssl feature" This reverts commit f5c390f57877d806f86f61b1903945d08c869463. --- library.json | 2 +- library.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library.json b/library.json index 3e5467e..8d07af7 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "AsyncTCP", - "version": "3.6.0", + "version": "3.5.0", "description": "Asynchronous TCP Library for ESP32", "keywords": "async,tcp", "repository": { diff --git a/library.properties b/library.properties index 47625ab..c54f294 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=Async TCP includes=AsyncTCP.h -version=3.6.0 +version=3.5.0 author=ESP32Async maintainer=ESP32Async sentence=Async TCP Library for ESP32 From 69d5e059121bbfbe182184c231a093d56b27c1af Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 12:47:46 -0700 Subject: [PATCH 12/21] memory leaks --- src/AsyncTCP.cpp | 14 ++++++++++++++ src/AsyncTCP.h | 3 +-- src/AsyncTCPTLS.cpp | 11 ++++++----- src/AsyncTCPTLS.h | 15 ++++++++++++++- src/AsyncTCPVersion.h | 2 +- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index c9ff13c..20c2b01 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1977,6 +1977,18 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { int8_t AsyncServer::_accepted(AsyncClient *client) { #if ASYNC_TCP_SSL_ENABLED if (_use_ssl && _cert && _key && client && client->pcb()) { + if (AsyncTCPTLS::getActiveCount() >= SSL_MAX_CONNECTIONS) { + async_tcp_log_e("SSL connection limit reached (%d/%d)", AsyncTCPTLS::getActiveCount(), SSL_MAX_CONNECTIONS); + client->abort(); + delete client; + return ERR_ABRT; + } + if (ESP.getFreeHeap() < 10000) { + async_tcp_log_e("SSL rejected: low heap (%u bytes)", ESP.getFreeHeap()); + client->abort(); + delete client; + return ERR_ABRT; + } AsyncTCPTLS *ssl = new (std::nothrow) AsyncTCPTLS(); if (ssl) { int ret = ssl->startSSLServer(client->pcb(), _cert, _cert_len, _key, _key_len, _ssl_key_password); @@ -1987,11 +1999,13 @@ int8_t AsyncServer::_accepted(AsyncClient *client) { async_tcp_log_e("startSSLServer failed: %d", ret); delete ssl; client->abort(); + delete client; return ERR_ABRT; } } else { async_tcp_log_e("Failed to allocate SSL context for server"); client->abort(); + delete client; return ERR_ABRT; } } diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 9ae84ae..942a6bc 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -73,8 +73,7 @@ class AsyncClient; #define ASYNC_WRITE_FLAG_MORE 0x02 // will not send PSH flag, meaning that there should be more data to be sent before the application should react. #if ASYNC_TCP_SSL_ENABLED -#define SSL_HANDSHAKE_TIMEOUT 10000 -class AsyncTCPTLS; +#include "AsyncTCPTLS.h" #endif typedef std::function AcConnectHandler; diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 7c6cbe7..c1c67fd 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -25,10 +25,6 @@ extern "C" { #define MBEDTLS_ERR_NET_SEND_FAILED -0x004E #endif -#ifndef SSL_HANDSHAKE_TIMEOUT -#define SSL_HANDSHAKE_TIMEOUT 10000 -#endif - // DER cache — PEM parsed once, reused for all server connections static unsigned char *_cached_cert_der = NULL; static size_t _cached_cert_der_len = 0; @@ -135,6 +131,7 @@ static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { mbedtls_ctr_drbg_context AsyncTCPTLS::drbg_ctx; mbedtls_entropy_context AsyncTCPTLS::entropy_ctx; bool AsyncTCPTLS::_conf_initialized = false; +int AsyncTCPTLS::_active_count = 0; void AsyncTCPTLS::_init_rng(void) { if (_conf_initialized) return; @@ -154,13 +151,15 @@ AsyncTCPTLS::AsyncTCPTLS(void) { _have_ca_cert = false; _have_client_cert = false; _have_client_key = false; - handshake_timeout = 120000; + handshake_timeout = SSL_HANDSHAKE_TIMEOUT; handshake_start_time = 0; _ssl_rx_buf = (unsigned char *)malloc(ASYNCTCP_TLS_RX_BUF_SIZE); _ssl_rx_buf_capacity = _ssl_rx_buf ? ASYNCTCP_TLS_RX_BUF_SIZE : 0; _ssl_rx_buf_len = 0; _ssl_rx_pos = 0; + + _active_count++; } AsyncTCPTLS::~AsyncTCPTLS() { @@ -180,6 +179,8 @@ AsyncTCPTLS::~AsyncTCPTLS() { free(_ssl_rx_buf); _ssl_rx_buf = NULL; } + + _active_count--; } bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 75b280f..d6111f7 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -18,7 +18,15 @@ struct tcp_pcb; #define ASYNCTCP_TLS_EOF(r) (((r) == MBEDTLS_ERR_SSL_CONN_EOF) || ((r) == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY)) #define ASYNCTCP_TLS_RX_BUF_SIZE 4096 -#define ASYNCTCP_TLS_RX_BUF_MAX 16384 +#define ASYNCTCP_TLS_RX_BUF_MAX 8192 + +#ifndef SSL_HANDSHAKE_TIMEOUT +#define SSL_HANDSHAKE_TIMEOUT 10000 +#endif + +#ifndef SSL_MAX_CONNECTIONS +#define SSL_MAX_CONNECTIONS 4 +#endif class AsyncTCPTLS { @@ -31,6 +39,9 @@ class AsyncTCPTLS static mbedtls_entropy_context entropy_ctx; static bool _conf_initialized; + // Concurrent connection tracking + static int _active_count; + static void _init_rng(void); mbedtls_x509_crt ca_cert; @@ -82,6 +93,8 @@ class AsyncTCPTLS // Diagnostic: log BIO buffer state void logBioState(const char *tag) const; + static int getActiveCount() { return _active_count; } + int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, diff --git a/src/AsyncTCPVersion.h b/src/AsyncTCPVersion.h index 5fdbea9..4dc4073 100644 --- a/src/AsyncTCPVersion.h +++ b/src/AsyncTCPVersion.h @@ -10,7 +10,7 @@ extern "C" { /** Major version number (X.x.x) */ #define ASYNCTCP_VERSION_MAJOR 3 /** Minor version number (x.X.x) */ -#define ASYNCTCP_VERSION_MINOR 6 +#define ASYNCTCP_VERSION_MINOR 5 /** Patch version number (x.x.X) */ #define ASYNCTCP_VERSION_PATCH 0 From 0fa4122c9fa376d494a36997d22e476233a7a5e2 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 14:10:44 -0700 Subject: [PATCH 13/21] example now vanilla ESP32 --- .../WebServerSSL.ino} | 96 +++++++++++-------- 1 file changed, 58 insertions(+), 38 deletions(-) rename examples/{AsyncWebServerSSL/AsyncWebServerSSL.ino => WebServerSSL/WebServerSSL.ino} (69%) diff --git a/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino b/examples/WebServerSSL/WebServerSSL.ino similarity index 69% rename from examples/AsyncWebServerSSL/AsyncWebServerSSL.ino rename to examples/WebServerSSL/WebServerSSL.ino index 7756567..d2c421b 100644 --- a/examples/AsyncWebServerSSL/AsyncWebServerSSL.ino +++ b/examples/WebServerSSL/WebServerSSL.ino @@ -1,13 +1,13 @@ /* - * AsyncTCPSSL - AsyncWebServerSSL example + * AsyncTCP - WebServerSSL example * - * ESP32 HTTPS server using ESPAsyncWebServer + AsyncTCPSSL. + * ESP32 HTTPS server using AsyncTCP + AsyncTCPTLS directly. * Serves a simple page and a JSON API over TLS 1.2 on port 443. * Includes a ready-to-use self-signed cert (key password: test123). * - * Regenerate your own: - * openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ - * -keyout server.key -out server.pem -days 3650 \ + * Regenerate your own (RSA-2048): + * openssl req -x509 -newkey rsa:2048 \ + * -passout pass:test123 -keyout server.key -out server.pem -days 3650 \ * -subj "/CN=esp32.local" \ * -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" * @@ -20,14 +20,13 @@ #include #include -#include const char *SSID = "YOUR_SSID"; const char *PASSWORD = "YOUR_PASSWORD"; -// Self-signed cert for esp32.local — key password: test123 -// Regenerate: openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ -// -keyout server.key -out server.pem -days 3650 \ +// Self-signed RSA-2048 cert for esp32.local — key password: test123 +// Regenerate: openssl req -x509 -newkey rsa:2048 \ +// -passout pass:test123 -keyout server.key -out server.pem -days 3650 \ // -subj "/CN=esp32.local" -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" static const char SERVER_CERT[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- @@ -84,15 +83,29 @@ CcL72QSXG4Pjb0QUXb8tnDfnrTPcffQbKgTrqvjFpPKDlATFaUeVz+8= -----END ENCRYPTED PRIVATE KEY----- )EOF"; -AsyncWebServer server(443); -AsyncWebServer httpserver(80); +AsyncServer *sslServer = NULL; -const char index_html[] PROGMEM = R"rawliteral( +static void sendResponse(AsyncClient *client, int code, const char *contentType, + const char *body, size_t bodyLen) { + char header[128]; + int headerLen = snprintf(header, sizeof(header), + "HTTP/1.1 %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %u\r\n" + "Connection: close\r\n" + "\r\n", + code == 200 ? "200 OK" : "301 Moved", + contentType, (unsigned)bodyLen); + client->write(header, headerLen); + client->write(body, bodyLen); +} + +static const char INDEX_PAGE[] PROGMEM = R"rawliteral( - AsyncWebServerSSL + WebServerSSL -

AsyncWebServerSSL

+

WebServerSSL

HTTPS is working!

/api - JSON endpoint

)rawliteral"; +void onClient(void *arg, AsyncClient *client) { + if (!client) return; + + client->onData([](void *arg, AsyncClient *client, void *data, size_t len) { + String request((const char *)data, len); + + if (request.startsWith("GET /api")) { + char json[128]; + int jsonLen = snprintf(json, sizeof(json), + "{\"status\":\"ok\",\"heap\":%u,\"uptime\":%lu}", + ESP.getFreeHeap(), millis() / 1000); + sendResponse(client, 200, "application/json", json, jsonLen); + } else { + sendResponse(client, 200, "text/html", INDEX_PAGE, sizeof(INDEX_PAGE) - 1); + } + client->close(); + }); + + client->onError([](void *arg, AsyncClient *client, int8_t error) { + Serial.printf("Connection error: %d\n", error); + delete client; + }); + + client->onDisconnect([](void *arg, AsyncClient *client) { + delete client; + }); +} + void setup() { Serial.begin(115200); delay(500); @@ -119,34 +160,13 @@ void setup() { } Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str()); - // Serve a simple HTML page - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send(200, "text/html", index_html); - }); - - // JSON API endpoint - server.on("/api", HTTP_GET, [](AsyncWebServerRequest *request) { - AsyncJsonResponse *response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - root["status"] = "ok"; - root["heap"] = ESP.getFreeHeap(); - root["uptime"] = millis() / 1000; - response->setLength(); - request->send(response); - }); + sslServer = new AsyncServer(443); + sslServer->onClient(onClient, NULL); + sslServer->beginSecure(SERVER_CERT, SERVER_KEY, "test123"); - // Start HTTPS server - server.beginSecure(SERVER_CERT, SERVER_KEY, "test123"); Serial.printf("[Server] HTTPS on port 443\n"); Serial.printf("Test: curl -k https://%s/api\n", WiFi.localIP().toString().c_str()); - - // Optional: HTTP-to-HTTPS redirect - httpserver.onNotFound([](AsyncWebServerRequest *request) { - request->redirect("https://" + WiFi.localIP().toString() + request->url()); - }); - httpserver.begin(); - Serial.printf("[Server] HTTP redirect on port 80\n"); } void loop() { From 885e255770a3aa3098e7d4ef8bf500fcee2d5e97 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 14:17:29 -0700 Subject: [PATCH 14/21] fix a bunch of Copilot complaints --- .github/workflows/ci.yml | 5 ++ platformio.ini | 7 +++ src/AsyncTCP.cpp | 3 -- src/AsyncTCP.h | 2 +- src/AsyncTCPTLS.cpp | 101 ++++++++++++++++++++------------------- src/AsyncTCPTLS.h | 4 +- 6 files changed, 66 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84266b5..8e1602d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,9 @@ jobs: - name: FetchWebsite run: arduino-cli --config-file ${{ matrix.config }} --library ./src/ compile --fqbn esp32:esp32:esp32 ./examples/FetchWebsite/FetchWebsite.ino + - name: WebServerSSL + run: arduino-cli --config-file ${{ matrix.config }} --library ./src/ compile --fqbn esp32:esp32:esp32 --build-property "compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1" ./examples/WebServerSSL/WebServerSSL.ino + platformio: name: "pio:${{ matrix.env }}:${{ matrix.board }}" runs-on: ubuntu-latest @@ -101,6 +104,8 @@ jobs: - run: PLATFORMIO_SRC_DIR=examples/Client PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - run: PLATFORMIO_SRC_DIR=examples/FetchWebsite PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} + - if: startsWith(matrix.env, 'ci-arduino-3') + run: PLATFORMIO_SRC_DIR=examples/WebServerSSL PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3-ssl esp-idf: name: Build with ESP-IDF ${{ matrix.idf_ver }} for ${{ matrix.idf_target }} diff --git a/platformio.ini b/platformio.ini index be7f5cc..c9c4a59 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,6 +53,13 @@ board = ${sysenv.PIO_BOARD} platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip board = ${sysenv.PIO_BOARD} +[env:ci-arduino-3-ssl] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip +board = ${sysenv.PIO_BOARD} +build_flags = + ${env.build_flags} + -D ASYNC_TCP_SSL_ENABLED=1 + [env:ci-arduino-2-esp-idf_log] platform = espressif32@6.12.0 board = ${sysenv.PIO_BOARD} diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 20c2b01..24b7b9c 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -1925,9 +1925,6 @@ void AsyncServer::end() { } _pcb = NULL; } -#if ASYNC_TCP_SSL_ENABLED - AsyncTCPTLS::_clear_DER_cache(); -#endif } // runs on LwIP thread diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 942a6bc..92f230a 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -7,7 +7,7 @@ #include "AsyncTCPVersion.h" #define ASYNCTCP_FORK_ESP32Async -// SSL support — default enabled, set to 0 before including this header to disable +// SSL support — default disabled; define as 1 globally at build time to enable #ifndef ASYNC_TCP_SSL_ENABLED #define ASYNC_TCP_SSL_ENABLED 0 #endif diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index c1c67fd..2bfc5d4 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -16,8 +16,8 @@ extern "C" { #if ASYNC_TCP_SSL_ENABLED #if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) -# warning "Please configure IDF framework to include mbedTLS -> Enable pre-shared-key ciphersuites and activate at least one cipher" -#else +# warning "PSK ciphersuites not configured — PSK TLS overloads will be unavailable" +#endif // From mbedtls/net_sockets.h — not included since we use custom LwIP BIO @@ -25,19 +25,6 @@ extern "C" { #define MBEDTLS_ERR_NET_SEND_FAILED -0x004E #endif -// DER cache — PEM parsed once, reused for all server connections -static unsigned char *_cached_cert_der = NULL; -static size_t _cached_cert_der_len = 0; -static unsigned char *_cached_key_der = NULL; -static size_t _cached_key_der_len = 0; - -void AsyncTCPTLS::_clear_DER_cache(void) { - if (_cached_cert_der) { free(_cached_cert_der); _cached_cert_der = NULL; } - _cached_cert_der_len = 0; - if (_cached_key_der) { free(_cached_key_der); _cached_key_der = NULL; } - _cached_key_der_len = 0; -} - static int _handle_error(int err) { if (err == -30848) { return err; @@ -295,6 +282,7 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return handle_error(ret); } } else if (pskIdent != NULL && psKey != NULL) { +#if defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) || defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) async_tcp_log_v("Setting up PSK"); if ((strlen(psKey) & 1) != 0 || strlen(psKey) > 2 * MBEDTLS_PSK_MAX_LEN) { async_tcp_log_e("pre-shared key not valid hex or too long"); @@ -323,6 +311,10 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, async_tcp_log_e("mbedtls_ssl_conf_psk returned %d", ret); return handle_error(ret); } +#else + async_tcp_log_e("PSK ciphersuites not enabled in mbedTLS config"); + return -1; +#endif } else { return -1; } @@ -424,29 +416,36 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, // Self-signed cert — no client certificate verification mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); - // Load server certificate — use DER cache if available + // Load server certificate mbedtls_x509_crt_init(&client_cert); - if (_cached_cert_der != NULL) { - async_tcp_log_v("Using cached cert DER (%u bytes)", (unsigned)_cached_cert_der_len); - ret = mbedtls_x509_crt_parse(&client_cert, _cached_cert_der, _cached_cert_der_len); - } else { - async_tcp_log_v("Parsing cert PEM -> DER (first connection, will cache)"); + if (server_cert_len >= 11 && memcmp(server_cert, "-----BEGIN ", 11) == 0) { + // PEM — make a NUL-terminated copy (mbedtls_pem_read_buffer requires it) + async_tcp_log_v("Parsing cert PEM (%u bytes)", (unsigned)server_cert_len); + char *pem_buf = (char *)malloc(server_cert_len + 1); + if (!pem_buf) { + _deleteHandshakeCerts(); + return handle_error(MBEDTLS_ERR_SSL_ALLOC_FAILED); + } + memcpy(pem_buf, server_cert, server_cert_len); + pem_buf[server_cert_len] = '\0'; mbedtls_pem_context pem; mbedtls_pem_init(&pem); size_t use_len = 0; + size_t cert_der_len = 0; ret = mbedtls_pem_read_buffer(&pem, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", - server_cert, NULL, 0, &use_len); + (const unsigned char *)pem_buf, NULL, 0, &use_len); if (ret == 0) { - const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &_cached_cert_der_len); - _cached_cert_der = (unsigned char *)malloc(_cached_cert_der_len); - if (_cached_cert_der) { - memcpy(_cached_cert_der, der_buf, _cached_cert_der_len); - } - ret = mbedtls_x509_crt_parse(&client_cert, der_buf, _cached_cert_der_len); + const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &cert_der_len); + ret = mbedtls_x509_crt_parse(&client_cert, der_buf, cert_der_len); } mbedtls_pem_free(&pem); + free(pem_buf); + } else { + // DER — parse directly with length + async_tcp_log_v("Parsing cert DER (%u bytes)", (unsigned)server_cert_len); + ret = mbedtls_x509_crt_parse(&client_cert, server_cert, server_cert_len); } _have_client_cert = true; if (ret < 0) { @@ -454,28 +453,31 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, return handle_error(ret); } - // Load server private key — use DER cache if available + // Load server private key mbedtls_pk_init(&client_key); - if (_cached_key_der != NULL) { - async_tcp_log_v("Using cached key DER (%u bytes)", (unsigned)_cached_key_der_len); - const unsigned char *pwd = (const unsigned char *)_ssl_key_password; - size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; - ret = mbedtls_pk_parse_key(&client_key, _cached_key_der, _cached_key_der_len, - pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); - } else { - async_tcp_log_v("Parsing key PEM -> DER (first connection, will cache)"); + if (server_key_len >= 11 && memcmp(server_key, "-----BEGIN ", 11) == 0) { + // PEM — make a NUL-terminated copy + async_tcp_log_v("Parsing key PEM (%u bytes)", (unsigned)server_key_len); + char *pem_buf = (char *)malloc(server_key_len + 1); + if (!pem_buf) { + _deleteHandshakeCerts(); + return handle_error(MBEDTLS_ERR_SSL_ALLOC_FAILED); + } + memcpy(pem_buf, server_key, server_key_len); + pem_buf[server_key_len] = '\0'; mbedtls_pem_context pem; mbedtls_pem_init(&pem); size_t use_len = 0; + size_t key_der_len = 0; const unsigned char *pwd = (const unsigned char *)_ssl_key_password; size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; const char *key_header, *key_footer; - if (strncmp((const char *)server_key, "-----BEGIN ENCRYPTED PRIVATE KEY-----", 37) == 0) { + if (strncmp(pem_buf, "-----BEGIN ENCRYPTED PRIVATE KEY-----", 37) == 0) { key_header = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; key_footer = "-----END ENCRYPTED PRIVATE KEY-----"; - } else if (strncmp((const char *)server_key, "-----BEGIN PRIVATE KEY-----", 27) == 0) { + } else if (strncmp(pem_buf, "-----BEGIN PRIVATE KEY-----", 27) == 0) { key_header = "-----BEGIN PRIVATE KEY-----"; key_footer = "-----END PRIVATE KEY-----"; } else { @@ -483,17 +485,19 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, key_footer = "-----END RSA PRIVATE KEY-----"; } ret = mbedtls_pem_read_buffer(&pem, key_header, key_footer, - server_key, pwd, pwd_len, &use_len); + (const unsigned char *)pem_buf, pwd, pwd_len, &use_len); if (ret == 0) { - const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &_cached_key_der_len); - _cached_key_der = (unsigned char *)malloc(_cached_key_der_len); - if (_cached_key_der) { - memcpy(_cached_key_der, der_buf, _cached_key_der_len); - } - ret = mbedtls_pk_parse_key(&client_key, der_buf, _cached_key_der_len, - NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); + const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &key_der_len); + ret = mbedtls_pk_parse_key(&client_key, der_buf, key_der_len, + pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); } mbedtls_pem_free(&pem); + free(pem_buf); + } else { + // DER — parse directly with length + async_tcp_log_v("Parsing key DER (%u bytes)", (unsigned)server_key_len); + ret = mbedtls_pk_parse_key(&client_key, server_key, server_key_len, + NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); } _have_client_key = true; if (ret != 0) { @@ -560,14 +564,12 @@ int AsyncTCPTLS::runSSLHandshake(void) { async_tcp_log_v("Certificate verification was skipped (expected for self-signed server): %s", buf); } else { async_tcp_log_e("Failed to verify peer certificate! verification info: %s", buf); - _deleteHandshakeCerts(); return handle_error(-1); } } else { async_tcp_log_v("Certificate verified."); } - _deleteHandshakeCerts(); async_tcp_log_v("Free internal heap after TLS %u", ESP.getFreeHeap()); return 0; @@ -660,5 +662,4 @@ void AsyncTCPTLS::_deleteHandshakeCerts(void) { } } -#endif #endif // ASYNC_TCP_SSL_ENABLED diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index d6111f7..6922a16 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -78,8 +78,6 @@ class AsyncTCPTLS AsyncTCPTLS(void); virtual ~AsyncTCPTLS(); - static void _clear_DER_cache(void); - // Feed encrypted data from TCP into BIO buffer (returns false if buffer full) bool feedRxData(const unsigned char *data, size_t len); size_t rxBufLen() const { return _ssl_rx_buf_len - _ssl_rx_pos; } @@ -97,8 +95,10 @@ class AsyncTCPTLS int startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip); +#if defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) || defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *pskIdent, const char *psKey); +#endif int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *rootCABuff, From d02fac9f3c0ce4c66113490d78a9921d7a572726 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 15:44:30 -0700 Subject: [PATCH 15/21] ClientSSL example + tweaks --- .github/workflows/ci.yml | 5 + examples/ClientSSL/ClientSSL.ino | 205 +++++++++++++++++++++++++ examples/WebServerSSL/WebServerSSL.ino | 125 ++++++++------- src/AsyncTCP.cpp | 27 +++- src/AsyncTCP.h | 7 +- src/AsyncTCPTLS.cpp | 33 ++-- src/AsyncTCPTLS.h | 8 +- 7 files changed, 327 insertions(+), 83 deletions(-) create mode 100644 examples/ClientSSL/ClientSSL.ino diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e1602d..a04992e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,9 @@ jobs: - name: WebServerSSL run: arduino-cli --config-file ${{ matrix.config }} --library ./src/ compile --fqbn esp32:esp32:esp32 --build-property "compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1" ./examples/WebServerSSL/WebServerSSL.ino + - name: ClientSSL + run: arduino-cli --config-file ${{ matrix.config }} --library ./src/ compile --fqbn esp32:esp32:esp32 --build-property "compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1" ./examples/ClientSSL/ClientSSL.ino + platformio: name: "pio:${{ matrix.env }}:${{ matrix.board }}" runs-on: ubuntu-latest @@ -106,6 +109,8 @@ jobs: - run: PLATFORMIO_SRC_DIR=examples/FetchWebsite PIO_BOARD=${{ matrix.board }} pio run -e ${{ matrix.env }} - if: startsWith(matrix.env, 'ci-arduino-3') run: PLATFORMIO_SRC_DIR=examples/WebServerSSL PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3-ssl + - if: startsWith(matrix.env, 'ci-arduino-3') + run: PLATFORMIO_SRC_DIR=examples/ClientSSL PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3-ssl esp-idf: name: Build with ESP-IDF ${{ matrix.idf_ver }} for ${{ matrix.idf_target }} diff --git a/examples/ClientSSL/ClientSSL.ino b/examples/ClientSSL/ClientSSL.ino new file mode 100644 index 0000000..5e2829c --- /dev/null +++ b/examples/ClientSSL/ClientSSL.ino @@ -0,0 +1,205 @@ +/* + * AsyncTCP - ClientSSL example + * + * ESP32 HTTPS client with mutual TLS (mTLS) using AsyncTCP + AsyncTCPTLS. + * Connects to a server, presents a client certificate, and verifies the + * server using a shared CA. The server must request client certificates + * for mTLS to take effect. + * + * Includes a ready-to-use test PKI (key password: test123): + * - CA cert — shared trust root for both client and server + * - Client cert + key — presented to the server during handshake + * + * Regenerate the full PKI: + * # CA + * openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ + * -keyout ca.key -out ca.pem -days 3650 -subj "/CN=ESP32 Test CA" + * + * # Server (signed by CA) + * openssl req -newkey rsa:2048 -passout pass:test123 \ + * -keyout server.key -out server.csr -subj "/CN=esp32-server.local" + * openssl x509 -req -in server.csr -CA ca.pem -passin pass:test123 \ + * -CAkey ca.key -CAcreateserial -out server.pem -days 3650 \ + * -extfile <(printf "subjectAltName=DNS:esp32-server.local,IP:192.168.4.1") + * + * # Client (signed by CA) + * openssl req -newkey rsa:2048 -passout pass:test123 \ + * -keyout client.key -out client.csr -subj "/CN=esp32-client" + * openssl x509 -req -in client.csr -CA ca.pem -passin pass:test123 \ + * -CAkey ca.key -CAcreateserial -out client.pem -days 3650 + * + * To test, run a server that requests client certs, e.g.: + * openssl s_server -cert server.pem -key server.key \ + * -pass pass:test123 -CAfile ca.pem -Verify 1 -www -port 4433 + * + * Compile with: + * compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1 + */ + +#include +#include + +const char *SSID = "YOUR_SSID"; +const char *PASSWORD = "YOUR_PASSWORD"; + +// -- Server to connect to ------------------------------------------- +const char *SERVER_HOST = "192.168.4.1"; +const uint16_t SERVER_PORT = 4433; + +// -- CA certificate (verifies the server) -------------------------- +// Shared trust root — signed both the server and client certs. +static const char CA_CERT[] PROGMEM = R"EOF( +-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIUYBESfy7ki1nvGjMvvvBRQZAxr7wwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNRVNQMzIgVGVzdCBDQTAeFw0yNjA4MjUyMjAzMDhaFw0z +NjA4MjIyMjAzMDlaMBgxFjAUBgNVBAMMDUVTUDMyIFRlc3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3h61RZ+58l8hPMKXUigsXWB+4DYCJQEBM +zUIAMitu16rV4JqD7AGZISRoSZTGrxjZOGScvj0RGx2e4Oj0v30lzKxVu0DrOUpd +RY2tcdH1AzsV2XYuJbbPSrq5xGjIX34jmYrD7zPjGAvCHehfJWVIYqCCrtWVPfTE +KcxSaZ/5HW5vua6XUS6Ub/L6Qo/QlAF1tI3sk+HV5HbwIuoQcfPopohrWLdGopPQ +1EdMYXccKdsYLV8IR2WMbDmAJlAsDWtl7YICdkUz5k4rh/q9EV48698BNEpyxEHj +rcbLnXrqQtGxYCMts5p9FeT9T95SpEEH6NjrToK71bi0pYGDxm/rAgMBAAGjUzBR +MB0GA1UdDgQWBBRu9+OwsLAchlQa+OrsbMZIkI1Y1zAfBgNVHSMEGDAWgBRu9+Ow +sLAchlQa+OrsbMZIkI1Y1zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQBYEOtModTG0OSGYk5ccrPYQqGcyaU6HZsDQHHWcoTSnhu+I3zIXPtQUffa +fM0T7+c/zWLkrLYr5Qphv0dPDc3YmTGO7IYcYLEnmFWsHbekKQNEruU0qoKwJhRa +p/tWlvDMCPDTavHQMXm2frvVq0uPMcEDiCfeffZSyMWrzVA06IuWuzdslX81KqIJ +WNjp/QUIa9p3XW47Uus/gV+hE12D/7WoR5iBXjttBQ5soafLSo8rBOZZ4noHbHl5 +DM+p2VJc/xX1z+NovEsSNjo3j/2buo/VT1s268yUlGmXMLmdnQkWqAqpJcfXEWmM +ak1VKOrDAFKqocv9/aFKD/UYXXjD +-----END CERTIFICATE----- +)EOF"; + +// -- Client certificate (presented to the server) ------------------- +// Signed by the CA above. The server uses this to authenticate us. +static const char CLIENT_CERT[] PROGMEM = R"EOF( +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUWjOHsiza3KlO+ibKFoKusZlDomowDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNRVNQMzIgVGVzdCBDQTAeFw0yNjA4MjUyMjAzMDhaFw0z +NjA4MjIyMjAzMDhaMBcxFTATBgNVBAMMDGVzcDMyLWNsaWVudDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMlwU6IT2Co6mFt8MDNc8apojJVuqhKx3RfI ++SR+3tJv3CI+lvf+smrvRINpEcESY7s2bMfwjhemfZ8aUyeye/xVxtuoH+JCa5Zg +nRWwM46V2TjsfmJBpQkmlZfv3laklBAkZpw3xrIL66zIwfqIynSmXBgN8PnGSsoC +jpN0t2pBVr/p1zxB5G76RMhYO9Eua2IEF1JsVUQ+/dih2a9RaSbWhWaLD2kbG/6m +T/vtZOTNULGggcxJRJgULkpsBCaTMGi1zUAzKVWL9KkulAxOlcNOapPJ7Xel1Jrq +8XOPQHB9MPpjOgiUnWt8IAdL0YCSpAsy5j7kxjg3W1fpQZre+CUCAwEAAaNCMEAw +HQYDVR0OBBYEFO8F1UueWWPj/6JewZ99AlXAi7zSMB8GA1UdIwQYMBaAFG7347Cw +sByGVBr46uxsxkiQjVjXMA0GCSqGSIb3DQEBCwUAA4IBAQBpn+tFzK588Hh/XPB/ +ptLInacSREi0AL9I2d9DJ4h9BxDw3d3Sjo3UTrUnOEovjUqiBkAyFW9ymSu6GYAI +Ll5IN4ZcHE+VwtnG5ncaqVE0mzT/81Whs6JCH7RMnhfBA7Cv+N8F5Jw0RXHns+yw +n8t6pQsj1pTGeRs1Twdu7+xO/m2iRu4iCSgpIuXdJvd7dOTHG12zyAN37V/PSdBg +VnlFbpZyYIoQmpZ+KhW4/05nUqzvncA9+DqJ2nh5Gn+7djQkWnUSxAin/+F7+pZp +/pCnwlOJ9zxmraFATp982R0GXqka5JiMEfH+J2p0OokRZ+ZWRsT6cTwOFDCviZXu +UOOh +-----END CERTIFICATE----- +)EOF"; + +// -- Client private key (encrypted, password: test123) ---------------- +static const char CLIENT_KEY[] PROGMEM = R"EOF( +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQI4Sut7buwL2xN7ZC +XzI6uQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEP7SbAUYIB8Tdwtb +sNGaBRYEggTQbv4XQxi36jLdzJuBkeOs6/DQP7SW4wuVQkqOzZuhWLyaSFKrbciq +fpv+cpqFvZHalMxj3Avuh/rjsZPdOKwo+wcfYoGnA76bF01sL2SFHkrkDA49BNcq +YAYiyYpl3w3Pw66iBhyeiLzbOohPipSrdwbGZvHdqnZ/7hF9CCFLFrKsy4ztiq7I +xuU2wYkAAz6FjRlERub4TY7HK4yTFLJ+wQR5vx1VRyXHkSK4F1rvcEIjh71v1hHC +xbkKEPM71sxjFWPerQS7lFJA5B90rgK0DFNG3HlHSBqFqs0TsdRKf3EaqJVdI939 +bbIrPeAV8cyxYwOGJzbbKOV0zU2MGDQG2lHtssk+TagN/jiXBlfYlal2nWXKHgAS +CZFpTpCCBldG44cg+mgWt/hEZVtctxxBpDcDi/aOTwX1kOzVplw+hm6TpGgo3rKB +XhlTLmZLvEk8fmnr4SB+1lWuu5LmsdDP2RWtU48SL8pRCuCEUOMEQCjYV050lXJ+ +jbutA2/rHLVGjfiilG8gCMrbdHVtEORDB2MjXzH597PgKCQJzrtPSxgL9QZoCXhL +QRhCa1nc0vFmyL8fPkcV35lBN+IfIJmrbO5NI14ZChwG5VkApttz7TJB9Yg2tbYF +VdoPMzSwZ2rWuS+rwBfFCWDmCcJvdxGcHiCtQN+mKx9vyao9fOAGjKiOiP7FPMfu +DMJNwJyjSFcXDNPUjt1GbUsgNA+aj6sj2lHu2YHiOpnMrkJAcT3B0pvO4qDUGeIC +y7oKrPspPAKLV6wac0UNe9dqGShEtWO4EXrPZ6EqAoV85yHzizGqVWGMkZ2SS1m5 +YCAKL43V+tgspJ1wQQHiGeIJe75qj8dnnGfdCH8ukvQhLjByAzWFcxV+E53Qncq7 +zriPNsNfFchw2crm9BgGnTgo2GD2wmMPfMqSwR2EjWFB2w3ROJhfSKwZ4PmGqu4R +sDIeqxSJGr1/6OwqPQJfsh8QojZ/6sSPJ4neRJ4oXIv0Jx2N6kyCIf7Njc+/McXg +AY0Zecrev46ii3JSpmXp8JHGLTFsshZepdqZCv8Ip+J0ZKgkTStZg3dRUuM4nati +Zcj1tZKCxpdbIkV+NSMQpeWqyGfShhZn9QHiPWplWqZQd1RgutSOK5CyFsUN+3Z2 +XqUO7z7inh846pPU1403oDGsdye96Fd3enYd+gZA/sT5onhOj3SBIqkM8PtTtmWF +uNz0LrYVL0HqdRRLjMlYVUDyzcGxoYay5jk0NrEL8CRUwKjQ/+/M5NfqDpvnJ6+2 +MZaIBCTUh9sOa7dBk4MUpYsz9IPTul9BlUh/R9JUqlUEelWwvMg3Z/CQNtqy/+Xp +R0WyusRbHrD06iquvLOwN2RLgKQ9fxxcNMuy8vcBLESd2XaYuA4zYpaif1KiUGd7 +mXrl9KnWSfFPzALUsDIs+TDzoL4LU6LTxj27MwaoOPsAhYmLcArIXUzx312tWNW/ +rrSdVTqATqPkG4Fwr91oHKGrXFe+VpNrWMia8ZkLVOSIPdaeLbonmJtSF31Nrum4 +9tGI7kqTLVCxp0IoeIhoMv4pvnMP5IioxL9du2kua9Kl17sruZzogzs6SewVfHNS +ndEmp7FQtj5TMYqfXM0aWtoFMyJPFVYusJBl+cK6R2DGon7GKsxTjVA= +-----END ENCRYPTED PRIVATE KEY----- +)EOF"; + +AsyncClient *sslClient = NULL; + +void onConnected(void *arg, AsyncClient *client) { + Serial.println("[Client] TLS handshake complete — sending request"); + + const char *request = + "GET / HTTP/1.1\r\n" + "Host: " SERVER_HOST "\r\n" + "Connection: close\r\n" + "\r\n"; + client->write(request, strlen(request)); +} + +void onData(void *arg, AsyncClient *client, void *data, size_t len) { + Serial.printf("[Client] Received %u bytes:\n", len); + Serial.write((const uint8_t *)data, len); + Serial.println(); +} + +void onDisconnect(void *arg, AsyncClient *client) { + Serial.println("[Client] Disconnected"); + client->close(true); +} + +void onError(void *arg, AsyncClient *client, int8_t error) { + Serial.printf("[Client] Error %d: %s\n", error, + client->errorToString(error)); +} + +void setup() { + Serial.begin(115200); + delay(500); + + WiFi.begin(SSID, PASSWORD); + Serial.print("Connecting to WiFi"); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str()); + + sslClient = new AsyncClient(); + sslClient->onConnect(onConnected); + sslClient->onData(onData); + sslClient->onDisconnect(onDisconnect); + sslClient->onError(onError); + + Serial.printf("[Client] Connecting to %s:%u\n", SERVER_HOST, SERVER_PORT); + + // --- With server verification (recommended) --- + bool ok = sslClient->beginSecure( + SERVER_HOST, SERVER_PORT, + CA_CERT, + CLIENT_CERT, CLIENT_KEY, + "test123"); + + // --- Without server verification (skip CA check) --- + // WARNING: do NOT use in production — defeats the purpose of TLS. + // bool ok = sslClient->beginSecure( + // SERVER_HOST, SERVER_PORT, + // NULL, // no CA — server cert is not verified + // CLIENT_CERT, CLIENT_KEY, + // "test123"); + + if (!ok) { + Serial.println("[Client] beginSecure failed"); + delete sslClient; + sslClient = NULL; + return; + } +} + +void loop() { + delay(10000); +} diff --git a/examples/WebServerSSL/WebServerSSL.ino b/examples/WebServerSSL/WebServerSSL.ino index d2c421b..6d01bba 100644 --- a/examples/WebServerSSL/WebServerSSL.ino +++ b/examples/WebServerSSL/WebServerSSL.ino @@ -3,13 +3,30 @@ * * ESP32 HTTPS server using AsyncTCP + AsyncTCPTLS directly. * Serves a simple page and a JSON API over TLS 1.2 on port 443. - * Includes a ready-to-use self-signed cert (key password: test123). + * Uses a CA-signed cert — the same CA that signs the ClientSSL example's + * client cert, so either example can verify the other. * - * Regenerate your own (RSA-2048): - * openssl req -x509 -newkey rsa:2048 \ - * -passout pass:test123 -keyout server.key -out server.pem -days 3650 \ - * -subj "/CN=esp32.local" \ - * -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" + * Includes a ready-to-use test PKI (key password: test123): + * - CA cert — shared trust root (matches ClientSSL example) + * - Server cert — signed by the CA, CN=esp32-server.local + * + * Regenerate the full PKI: + * # CA + * openssl req -x509 -newkey rsa:2048 -passout pass:test123 \ + * -keyout ca.key -out ca.pem -days 3650 -subj "/CN=ESP32 Test CA" + * + * # Server (signed by CA) + * openssl req -newkey rsa:2048 -passout pass:test123 \ + * -keyout server.key -out server.csr -subj "/CN=esp32-server.local" + * openssl x509 -req -in server.csr -CA ca.pem -passin pass:test123 \ + * -CAkey ca.key -CAcreateserial -out server.pem -days 3650 \ + * -extfile <(printf "subjectAltName=DNS:esp32-server.local,IP:192.168.4.1") + * + * # Client (signed by CA) + * openssl req -newkey rsa:2048 -passout pass:test123 \ + * -keyout client.key -out client.csr -subj "/CN=esp32-client" + * openssl x509 -req -in client.csr -CA ca.pem -passin pass:test123 \ + * -CAkey ca.key -CAcreateserial -out client.pem -days 3650 * * Test with: * curl -k https:///api @@ -24,62 +41,60 @@ const char *SSID = "YOUR_SSID"; const char *PASSWORD = "YOUR_PASSWORD"; -// Self-signed RSA-2048 cert for esp32.local — key password: test123 -// Regenerate: openssl req -x509 -newkey rsa:2048 \ -// -passout pass:test123 -keyout server.key -out server.pem -days 3650 \ -// -subj "/CN=esp32.local" -addext "subjectAltName=DNS:esp32.local,IP:192.168.4.1" +// CA-signed server cert — key password: test123 +// Signed by "ESP32 Test CA" (same CA as ClientSSL example) static const char SERVER_CERT[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- -MIIDKzCCAhOgAwIBAgIUdcrSmRQHfj1jGuDmkB1xOVbdt64wDQYJKoZIhvcNAQEL -BQAwFjEUMBIGA1UEAwwLZXNwMzIubG9jYWwwHhcNMjYwODI1MTkwMzU1WhcNMzYw -ODIyMTkwMzU1WjAWMRQwEgYDVQQDDAtlc3AzMi5sb2NhbDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBALHDfpBW6MqSH55rLLFuY5eHYi5S/bH3du5Jdw5c -t7qmc55pIbe0zMBCHKzfua8KI/0OWq/kJBynXcda680pRJ5u9AVmqXEqVQxJ6FW/ -vHtRDdBz7rEcGr1rtzmnP6KTxjaXc9AZPQloz18dTyxSwJq1h8sdl7Vtoz5bQzeG -FR2bWZU5z8PUeddVVsX9oUFAC2AfzV5wAC1KH+F8KRmVbGoon4T5T0dE66Sz2DQY -J8BvJUCjTh2E1r+tR8Y3wN9L1sw2InHsPOH1mzWNI2gBoWIqrHL9GSuLfYfUstbn -qA3eoeILluph/3s8XE7oLiEveeo3l1XVI0dZLBRcPcqNmYMCAwEAAaNxMG8wHQYD -VR0OBBYEFABAm1DHsG64G9CdObCOauNH9jKoMB8GA1UdIwQYMBaAFABAm1DHsG64 -G9CdObCOauNH9jKoMA8GA1UdEwEB/wQFMAMBAf8wHAYDVR0RBBUwE4ILZXNwMzIu -bG9jYWyHBMCoBAEwDQYJKoZIhvcNAQELBQADggEBADvl8Gf13zSArF1dCgXFDtPs -xdnbTDT/dtLgOFA85JxPWCizzzAKJElU1ZuufOjOocTM8bxEv7XpicTHaK5RxNAi -77oAMYYeU5HcsU127gMrLpA7H9DU7MulwKfadGw5aa22YD8Fbue50SEx3OE5jHpL -++IMPKF/HoN5GyP5+yhd8HjadnA1BVv3ylAn+ZOH4szcUZ+vRb9OZsoWgjeiph1c -bEP7VWWNxcZSJS/geZP2xvZoSXgOGlzGfFz+MnY3TyIwo6O8W6uYHCQ7us8kEW4V -O6o3be94uiadaVZzhHe8i97oSUqpJHpvXce1r0rP/d7m78/2LDUX+YPYaV+D2X8= +MIIDKjCCAhKgAwIBAgIUWjOHsiza3KlO+ibKFoKusZlDomkwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNRVNQMzIgVGVzdCBDQTAeFw0yNjA4MjUyMjAzMDhaFw0z +NjA4MjIyMjAzMDhaMB0xGzAZBgNVBAMMEmVzcDMyLXNlcnZlci5sb2NhbDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ54LgM4gGRTx/dPkvKCLTr/4vwk +NmNdzr7H7+iWpX/gfYwbWuGMK1kt8Uh0+Xpe+KmzP5osXXb6WiAOtwW2V3lz4oR5 +ZpRHxUsR+Egt4Q+miMdMsoP4+ENH/VI3UTIURkwTNDsmBBH6t6Dejrv59AuMBRob +/QUzfWZ+5+sZGPppEMrK7UBqe+QOTX7rAelwJaWcC1lqxfRFIp9Tj83KqvDMj178 +KpS0ZYEvsrYvOwhUEAG5SocP0JIPhTzLyuyx149noJyMV1s8bKi7hX3C2UejkH9d +yJNCsLiRzzMs+o/zpEM6WEQg8qMrdJY5eETjXGjnSjR3J8KrhwtZxp+QX1MCAwEA +AaNnMGUwIwYDVR0RBBwwGoISZXNwMzItc2VydmVyLmxvY2FshwTAqAQBMB0GA1Ud +DgQWBBTjuvVuucw6NeE9rdhIDxPoK3NFvjAfBgNVHSMEGDAWgBRu9+OwsLAchlQa ++OrsbMZIkI1Y1zANBgkqhkiG9w0BAQsFAAOCAQEAG5jRgelDQmkwlifEgh7AY9G4 +6afVDMUxJ8d6GPRjhEYQM9jadXWaz+z0tCOo4fUbwfFxt0CBaUFfR2MEv7w7HXfY +hEydW4/Q3kMPr27f+R8kl3f34cfcOQS+RHgN87q4i+pWN2l59si/hXh7mvdmmSkd +dwQsDEtE2yvdNpQsh2ZsKJg06EM6Lb0VK89UHFP26yJ1+PG1LRy5YzoodozwkH0R +DwMOd4eGnmCfcdlgrX4teHED3rqveP0E6wCLVXRZDACtdqhfGR4v2HfX6w2pCeH9 +eLW5cKlXgxh+204wb+IRHpiIgVRQONjVe2K8oF5Xtmglk6Znx0Zb/7jysMElwA== -----END CERTIFICATE----- )EOF"; static const char SERVER_KEY[] PROGMEM = R"EOF( -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQT1LwhLF3QJtPmEPo -paqR8gICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEBrUrlk3/9b7bU6b -5Z2H7doEggTQiN3qZeQO4fRXqx1TwoAdgOq37uqbkCPf5zXPg9oLNH85S7E9ndfS -x0PnUizxZ/BXsoTf28B5yqXnSMOsaeJr6aIHx9V5is+rVOQfdY8Rmt+xyidmhmC7 -o4FJd3DWzF7aQi/PMJrNY5r3EnAqxxPy3kwjlfo42+ubYXHrB4xS4dHaQv/MoMq1 -Mxb+SjP4/UcO+97zer6wSogDZdxQO+uvt37Y0gMKfpTn2QJnDnuu8WN/b1EKcSFm -jwEoLVIfF8J2i5W03zWCmVMK568R7eEkGDq4/aMnScEN2tYWt3gPk4kNK3qQ1fyN -PbobbtSQzstQ4sEiylZkL9JDHISJu7KNUc6JLZlUyhhmF1eTMWyWzUaXuO+OjRoq -JBdAPSjyJQDLiGIcfI4xuqXDssqTQy9WqKkbXCIBloahDPyM8akW7FgCT8gDw9i+ -DiGJzyVTNfr7X3OWCJ5tuyCpQI4Mkyi5et1p0ESOS+ctfvRKe9yvRmpI0Kvrzs5S -40TncsQJi9WpFPOrE0BgpftMGN1fW2Mq7R43eZ29vMfY4WFf9yeMV73waFcXH621 -dTnf8eDae0NMM3IpGher8hDtpVSdh3Np3VZsrtPRIdcAFjOodwLFgUzk1DXT+tvB -HJOUJwEd3tLHmxo3cuXn09SOY4cuHckka5Cmo9wkQxkCx8lCjdNgGmAa7op5WSvs -MTwij+s1eNk7mMZce+CFHRLU8tcl05fqI/tCEfjRwBa25vKMxG8/g19/HVANaMnL -tGS+ao3YeOxrFNzSYYB6FT16dc/P1g32BSIRnEgEULYFxQDhl/A+oiTDSHXoLl9d -djCvdMi2zX89UZ4sIZWgSYu4UlhQzLiOt7Fa4EO/S4Z5WimR9ag7yDZCtCSWBeUg -7dbGx4tgU4sLvF+hKAVcreHJoBeyWblL1JaY2NbLLJ8yucbV6MuvtQLmgO5pKF27 -WOjPVHlQ6mMCcJ8FX8SuT6F/4ZeKAHOHQRTM85Ai9WBPaCnGlLoIK6DwOU+7LoV3 -smWq4cx7qGUo2Nx7laITFxy4zggqsIxHJ9tckQDWkGHOClT+nrVhsBF59d/i4uen -3REGhW0IpJU/OhZnmbawDMh6nSW3VSQhE4wkC6lr0pFnOtlnYJzpdHibpQoUO8DD -SxzL8AG5Hdz/eORny88/a7kivmEFWQ3L1SX9cqHq5zXvuFWv6bYvKhhrwWUh13HK -MeeoeKqDGF+rt1jznZjcHJmSq5QI2HoaOhhj7QAUai72mi1b8ZtwYHF+ORnp1oi0 -vCw9RKiidHYFQ3ugC5OreBvElzWBjv2Xlf2nHtBhvXFRzTowmEUORMlrWAEJ7ByB -QH72J8GsLR9+NNJiGPJ3OreQ6TsIhmJycyjOlfyz9qK5faz6XnoBzqdGe4iwWd1n -UkT9OrK6lrb1zIKRzVXgxKdopSQ6RKkk7x8SYyTZwLJwSBIx19tZJh7wmRVMNToz -SXOgJAcjB8T1Dz0eaiDT0kwFiaSqwngQXo70x+rKsriRwJ9EXrJ5OQZ61YWj75GJ -ynLof9teDiX4IxOY8brkJqAIkFasjQggJx9PAmEWOodHUinfmy3ppKDEjmmmlngN -CcL72QSXG4Pjb0QUXb8tnDfnrTPcffQbKgTrqvjFpPKDlATFaUeVz+8= +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQOk5aidzXHFPL3jnP +ZjJ44gICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEO/+XceRF3kQ/0fe +irdpEBoEggTQ2PISec4Ww7Vn+hf07GWiizPu2OiC/OzRUqBwpKytkevdOFLSAyyR +jzqhIYqB4vzarTkVocJFmhJbO3pI6DWv7n3KaiajH5DJQ6w7FJd52ix9abzlgjQd +z0rnCTQR/Cjh3y2UPgJLozfBtrKHEr6d5MSEarWCS7IkDyzTwaoTNS5aWtcl+uKz +XCVfEqXiN4AaQcHQFL36nfX63Vash7xW5VCnafpvYd9nuvms2N4eklxv8YY8FQ8Q +xI4Ql5f1G1wApR5EQKuc3jvnw7av9gcg9tiykIDWfKtp1Kw+yuWfG8HMWVVqCThU +2bZOICFALSJjw/R0LQXxl0s7stki9tkzW9WOueCYyEgjjwSmGRCJt+W677ZOLN2e +knY42ifwr45qIea/a+sJ+GvFjcfI2+skxw+VPcViB1sdWdI7aWBEiBMkgcwymZi9 +uu0aY9HXJeNhpaCl556kPFoBc4kvHJ5SsDv85BDnC+WmdKkI0qjNlPwhNhjB4WpT +FO+xHp4Q5OPikSeWjYiE8uWpQQn0tzC3cxrlVEaG3fuSRld3pHvs8GbfC26f+X/0 +rexm76/Gj4PV2OJpgCcu1q+QeOrvUajtdAHqmubdErP3jNNN/LFtNZUgYL6kFUKR +iaK5iRiijYFHn4pK5LHaDXd8x8D5Np5S2q60nHxhGDHmdm1xv2ZxacYX03gy4g5G +7EcBREuK2IQHLQ5hR3Rxlnxa/7L65X6pRUk2izLv1c0w+5mRNyfW8LhqpawMqMNc +EJKlzEf/z/so1Mlwm+COld3mF7mwYSiwP8isUtXx35S3YkpZ+L0PFI2kQDqdupvR +rO0P0HMGVchZtkkl40GlZ6oyZ99ZqxGkNip+ec0yeGtJcE4gEYAnORC2bZcgKM1f +B7ZCacFO64EOrmD0murajgo6xyv9i5ITrVMMXewXMl/VO65DlYScgwJtUnOHyLK+ +WFmcUBSKgE89+I6nY6Q2URGC6V55GcADLL0+1EwCDmVbwWksouvh8oZWiDQDIprq +LBisfFoiBzzubIVcRutaSiQ5rBjRLWJFJjGmTxFopVIG7tHwAhqwtipf5pMR0SLA +zX7xO/cbTtKj0tIrKcRLejwegMoP/YsNueHhyAeZDmsVNVjt2XN9kydGFgNa9lO0 +z1V23DUQBIWRO6SgOWceexrrMLaYVSneKD6DPq4itcZUdBcZMVX+zToZNnZAp/XO +22UvTjrmDkwfls3kK/7UA8ar1JtWSXNL5IZaWxQhylabClwrg33kE7sBe7QBIrYh +v5n+Din7H9WRY1TgJBTPSlAqLX6VBRG1S+032ZdxKtzXNhscuopV3AdVxAULMVz3 +wXEWuJNtY81+nfTXseiyKFBjZTs7FufAYWhgYWk+BweagOQEGuPaH6VGXZsmuScF +3IRZmCUzWs2NIdFQA8gVq+SKdyUUq5BBm35UdrWSX5uDUFNcSW6tJpyzeM9IWClP +CtUJiwFC0aM4Q1RVxr7Jz1Q/TPAA6MMV03Ac5UGCfbRYMuJfQTklk5kQRZzsx+Sq +lTBcpdzS+qPAH9M8SQ5mz4cV1QqfCV5SCwhIAC7N/ZmYuq7/e9sEck1+Bv1CBjag +8oUFYmyM1jyCQc3l+vmXEoYItLlbjSwHfLrzywLXrTn2iwdt2alQE00= -----END ENCRYPTED PRIVATE KEY----- )EOF"; diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 24b7b9c..31e7e70 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -792,6 +792,7 @@ AsyncClient::AsyncClient(tcp_pcb *pcb) _ssl_client_cert_len = 0; _ssl_client_key = 0; _ssl_client_key_len = 0; + _ssl_key_password = NULL; _ssl_pending_pbufs = NULL; #endif if (_pcb) { @@ -810,6 +811,7 @@ AsyncClient::~AsyncClient() { delete _ssl_ctx; _ssl_ctx = 0; } + if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } #endif if (_pcb) { _close(); @@ -973,20 +975,23 @@ void AsyncClient::_clearSSLParams(void) { _ssl_client_cert_len = 0; _ssl_client_key = NULL; _ssl_client_key_len = 0; + if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } } bool AsyncClient::beginSecure(const char *host, uint16_t port, const char *rootCA, - const char *clientCert, const char *clientKey) { + const char *clientCert, const char *clientKey, const char *keyPassword) { return beginSecure(host, port, (const unsigned char *)rootCA, (rootCA != NULL) ? strlen(rootCA) + 1 : 0, (const unsigned char *)clientCert, (clientCert != NULL) ? strlen(clientCert) + 1 : 0, - (const unsigned char *)clientKey, (clientKey != NULL) ? strlen(clientKey) + 1 : 0); + (const unsigned char *)clientKey, (clientKey != NULL) ? strlen(clientKey) + 1 : 0, + keyPassword); } bool AsyncClient::beginSecure(const char *host, uint16_t port, const unsigned char *rootCA, size_t rootCALen, const unsigned char *clientCert, size_t clientCertLen, - const unsigned char *clientKey, size_t clientKeyLen) { + const unsigned char *clientKey, size_t clientKeyLen, + const char *keyPassword) { if (_ssl_ctx) { async_tcp_log_d("already have SSL context"); return false; @@ -999,6 +1004,8 @@ bool AsyncClient::beginSecure(const char *host, uint16_t port, _ssl_client_cert_len = clientCertLen; _ssl_client_key = clientKey; _ssl_client_key_len = clientKeyLen; + if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; return connect(host, port); } @@ -1175,10 +1182,16 @@ int8_t AsyncClient::_connected(tcp_pcb *pcb, int8_t err) { } return ERR_ABRT; } - int ret = _ssl_ctx->startSSLClient(_pcb, _ssl_host.c_str(), - _ssl_ca_cert, _ssl_ca_cert_len, - _ssl_client_cert, _ssl_client_cert_len, - _ssl_client_key, _ssl_client_key_len); + int ret; + if (_ssl_ca_cert == NULL) { + ret = _ssl_ctx->startSSLClientInsecure(_pcb, _ssl_host.c_str()); + } else { + ret = _ssl_ctx->startSSLClient(_pcb, _ssl_host.c_str(), + _ssl_ca_cert, _ssl_ca_cert_len, + _ssl_client_cert, _ssl_client_cert_len, + _ssl_client_key, _ssl_client_key_len, + _ssl_key_password); + } if (ret != 0) { async_tcp_log_e("startSSLClient failed: %d", ret); delete _ssl_ctx; diff --git a/src/AsyncTCP.h b/src/AsyncTCP.h index 92f230a..79cebde 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -284,11 +284,13 @@ class AsyncClient { #if ASYNC_TCP_SSL_ENABLED // SSL support bool beginSecure(const char *host, uint16_t port, const char *rootCA, - const char *clientCert = NULL, const char *clientKey = NULL); + const char *clientCert = NULL, const char *clientKey = NULL, + const char *keyPassword = NULL); bool beginSecure(const char *host, uint16_t port, const unsigned char *rootCA, size_t rootCALen, const unsigned char *clientCert = NULL, size_t clientCertLen = 0, - const unsigned char *clientKey = NULL, size_t clientKeyLen = 0); + const unsigned char *clientKey = NULL, size_t clientKeyLen = 0, + const char *keyPassword = NULL); bool ssl() const { return _ssl_ctx != 0; } void setSSLReceiveTimeout(uint32_t timeout) { _ssl_timeout = timeout; } uint32_t getSSLReceiveTimeout() const { return _ssl_timeout; } @@ -352,6 +354,7 @@ class AsyncClient { size_t _ssl_client_cert_len; const unsigned char *_ssl_client_key; size_t _ssl_client_key_len; + char *_ssl_key_password; void _clearSSLParams(void); pbuf *_ssl_pending_pbufs; #endif diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 2bfc5d4..c49a684 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -206,8 +206,7 @@ int AsyncTCPTLS::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { NULL, 0, NULL, 0, NULL, 0, - NULL, NULL, - true); + NULL, NULL); } int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, @@ -216,30 +215,32 @@ int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, NULL, 0, NULL, 0, NULL, 0, - pskIdent, psKey, - false); + pskIdent, psKey); } int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *rootCABuff, const char *cli_cert, - const char *cli_key) { + const char *cli_key, + const char *keyPassword) { return startSSLClient(pcb, host_or_ip, (const unsigned char *)rootCABuff, (rootCABuff != NULL) ? strlen(rootCABuff) + 1 : 0, (const unsigned char *)cli_cert, (cli_cert != NULL) ? strlen(cli_cert) + 1 : 0, - (const unsigned char *)cli_key, (cli_key != NULL) ? strlen(cli_key) + 1 : 0); + (const unsigned char *)cli_key, (cli_key != NULL) ? strlen(cli_key) + 1 : 0, + keyPassword); } int AsyncTCPTLS::startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, - const unsigned char *cli_key, const size_t cli_key_len) { + const unsigned char *cli_key, const size_t cli_key_len, + const char *keyPassword) { return _startSSLClient(pcb, host_or_ip, rootCABuff, rootCABuff_len, cli_cert, cli_cert_len, cli_key, cli_key_len, NULL, NULL, - false); + keyPassword); } int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, @@ -247,13 +248,9 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *cli_cert, const size_t cli_cert_len, const unsigned char *cli_key, const size_t cli_key_len, const char *pskIdent, const char *psKey, - bool insecure) { + const char *keyPassword) { int ret; - if (rootCABuff == NULL && pskIdent == NULL && psKey == NULL && !insecure) { - return -1; - } - if (!pcb) { return -1; } @@ -267,10 +264,10 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return handle_error(ret); } - if (insecure) { + if (rootCABuff == NULL) { mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); async_tcp_log_i("WARNING: Skipping SSL Verification. INSECURE!"); - } else if (rootCABuff != NULL) { + } else { async_tcp_log_v("Loading CA cert"); mbedtls_x509_crt_init(&ca_cert); mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); @@ -332,7 +329,11 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, } async_tcp_log_v("Loading private key"); - ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); + if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; + const unsigned char *pwd = (const unsigned char *)_ssl_key_password; + size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; + ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); _have_client_key = true; if (ret != 0) { _deleteHandshakeCerts(); diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 6922a16..93dd2e4 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -70,7 +70,7 @@ class AsyncTCPTLS const unsigned char *cli_cert, const size_t cli_cert_len, const unsigned char *cli_key, const size_t cli_key_len, const char *pskIdent, const char *psKey, - bool insecure); + const char *keyPassword = NULL); void _deleteHandshakeCerts(void); @@ -103,12 +103,14 @@ class AsyncTCPTLS int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const char *rootCABuff, const char *cli_cert, - const char *cli_key); + const char *cli_key, + const char *keyPassword = NULL); int startSSLClient(tcp_pcb *pcb, const char *host_or_ip, const unsigned char *rootCABuff, const size_t rootCABuff_len, const unsigned char *cli_cert, const size_t cli_cert_len, - const unsigned char *cli_key, const size_t cli_key_len); + const unsigned char *cli_key, const size_t cli_key_len, + const char *keyPassword = NULL); int startSSLServer(tcp_pcb *pcb, const unsigned char *server_cert, size_t server_cert_len, From 2bee84caa7d7f1fb76f646e013dfbde71ee604f4 Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 15:52:00 -0700 Subject: [PATCH 16/21] webserver as AP and client STA so they can connect to each other --- examples/ClientSSL/ClientSSL.ino | 6 +++--- examples/WebServerSSL/WebServerSSL.ino | 16 ++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/examples/ClientSSL/ClientSSL.ino b/examples/ClientSSL/ClientSSL.ino index 5e2829c..f702937 100644 --- a/examples/ClientSSL/ClientSSL.ino +++ b/examples/ClientSSL/ClientSSL.ino @@ -39,12 +39,12 @@ #include #include -const char *SSID = "YOUR_SSID"; -const char *PASSWORD = "YOUR_PASSWORD"; +const char *SSID = "ESP32-AP"; +const char *PASSWORD = "12345678"; // -- Server to connect to ------------------------------------------- const char *SERVER_HOST = "192.168.4.1"; -const uint16_t SERVER_PORT = 4433; +const uint16_t SERVER_PORT = 443; // -- CA certificate (verifies the server) -------------------------- // Shared trust root — signed both the server and client certs. diff --git a/examples/WebServerSSL/WebServerSSL.ino b/examples/WebServerSSL/WebServerSSL.ino index 6d01bba..82a3801 100644 --- a/examples/WebServerSSL/WebServerSSL.ino +++ b/examples/WebServerSSL/WebServerSSL.ino @@ -38,8 +38,8 @@ #include #include -const char *SSID = "YOUR_SSID"; -const char *PASSWORD = "YOUR_PASSWORD"; +const char *AP_SSID = "ESP32-AP"; +const char *AP_PASSWORD = "12345678"; // min 8 chars // CA-signed server cert — key password: test123 // Signed by "ESP32 Test CA" (same CA as ClientSSL example) @@ -167,13 +167,9 @@ void setup() { Serial.begin(115200); delay(500); - WiFi.begin(SSID, PASSWORD); - Serial.print("Connecting to WiFi"); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str()); + WiFi.softAP(AP_SSID, AP_PASSWORD); + delay(500); + Serial.printf("\nAP SSID: %s IP: %s\n", AP_SSID, WiFi.softAPIP().toString().c_str()); sslServer = new AsyncServer(443); sslServer->onClient(onClient, NULL); @@ -181,7 +177,7 @@ void setup() { Serial.printf("[Server] HTTPS on port 443\n"); Serial.printf("Test: curl -k https://%s/api\n", - WiFi.localIP().toString().c_str()); + WiFi.softAPIP().toString().c_str()); } void loop() { From a36650df128df3b28979b5a05087c8ddc3f7143b Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 16:43:06 -0700 Subject: [PATCH 17/21] some strange bugs creeped in --- src/AsyncTCP.cpp | 6 +++--- src/AsyncTCPTLS.cpp | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 31e7e70..5038243 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -811,7 +811,7 @@ AsyncClient::~AsyncClient() { delete _ssl_ctx; _ssl_ctx = 0; } - if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + if (_ssl_key_password) { ::free((void*)_ssl_key_password); _ssl_key_password = NULL; } #endif if (_pcb) { _close(); @@ -975,7 +975,7 @@ void AsyncClient::_clearSSLParams(void) { _ssl_client_cert_len = 0; _ssl_client_key = NULL; _ssl_client_key_len = 0; - if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + if (_ssl_key_password) { ::free((void*)_ssl_key_password); _ssl_key_password = NULL; } } bool AsyncClient::beginSecure(const char *host, uint16_t port, const char *rootCA, @@ -1004,7 +1004,7 @@ bool AsyncClient::beginSecure(const char *host, uint16_t port, _ssl_client_cert_len = clientCertLen; _ssl_client_key = clientKey; _ssl_client_key_len = clientKeyLen; - if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + if (_ssl_key_password) { ::free((void*)_ssl_key_password); _ssl_key_password = NULL; } _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; return connect(host, port); } diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index c49a684..870572a 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -158,7 +158,7 @@ AsyncTCPTLS::~AsyncTCPTLS() { mbedtls_ssl_config_free(&ssl_conf); if (_ssl_key_password) { - free(_ssl_key_password); + free((void*)_ssl_key_password); _ssl_key_password = NULL; } @@ -267,7 +267,7 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, if (rootCABuff == NULL) { mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); async_tcp_log_i("WARNING: Skipping SSL Verification. INSECURE!"); - } else { + } else if (rootCABuff != NULL) { async_tcp_log_v("Loading CA cert"); mbedtls_x509_crt_init(&ca_cert); mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); @@ -316,7 +316,7 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return -1; } - if (!insecure && cli_cert != NULL && cli_key != NULL) { + if (rootCABuff != NULL && cli_cert != NULL && cli_key != NULL) { mbedtls_x509_crt_init(&client_cert); mbedtls_pk_init(&client_key); @@ -329,7 +329,7 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, } async_tcp_log_v("Loading private key"); - if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + if (_ssl_key_password) { free((void*)_ssl_key_password); _ssl_key_password = NULL; } _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; const unsigned char *pwd = (const unsigned char *)_ssl_key_password; size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; @@ -384,7 +384,7 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, return -1; } - if (_ssl_key_password) { free(_ssl_key_password); _ssl_key_password = NULL; } + if (_ssl_key_password) { free((void*)_ssl_key_password); _ssl_key_password = NULL; } _ssl_key_password = password ? strdup(password) : NULL; async_tcp_log_v("Seeding the random number generator (server)"); From bda3ab75779a6a664ff6b18533005df0bcc4318b Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 25 Aug 2026 17:43:20 -0700 Subject: [PATCH 18/21] optimizing for speed --- src/AsyncTCP.cpp | 35 ++++++++++++++++++++++++------ src/AsyncTCPTLS.cpp | 52 +++++++++++++++++++++++---------------------- src/AsyncTCPTLS.h | 3 --- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/AsyncTCP.cpp b/src/AsyncTCP.cpp index 5038243..8ca9290 100644 --- a/src/AsyncTCP.cpp +++ b/src/AsyncTCP.cpp @@ -416,10 +416,23 @@ static void _bind_tcp_callbacks(tcp_pcb *pcb, AsyncClient *client) { tcp_poll(pcb, &AsyncTCP_detail::tcp_poll, CONFIG_ASYNC_TCP_POLL_TIMER); } +// Drain callback: ACKs and drops any late data arriving after close is initiated. +// Without this, LwIP sends RST when data arrives on a PCB with tcp_recv=NULL. +static err_t _tcp_drain_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { + (void)arg; (void)err; + if (p) { + tcp_recved(pcb, p->tot_len); + pbuf_free(p); + } else { + // NULL pbuf = remote closed — safe to ignore, close already in progress + } + return ERR_OK; +} + static void _reset_tcp_callbacks(tcp_pcb *pcb, AsyncClient *client) { tcp_arg(pcb, NULL); tcp_sent(pcb, NULL); - tcp_recv(pcb, NULL); + tcp_recv(pcb, _tcp_drain_recv); // drain late data instead of RST tcp_err(pcb, NULL); tcp_poll(pcb, NULL, 0); if (client) { @@ -656,6 +669,10 @@ static err_t _tcp_close_api(struct tcpip_api_call_data *api_call_msg) { if (*msg->pcb) { tcp_pcb *pcb = *msg->pcb; _reset_tcp_callbacks(pcb, msg->close); + // Flush pending output before close — gives tcp_close the best chance + // of succeeding. Without this, queued data forces tcp_close to fail, + // and the tcp_shutdown fallback sends FIN with unACKed data → RST. + tcp_output(pcb); if (tcp_close(pcb) != ERR_OK) { // tcp_close fails when unsent data remains (e.g. HTTP response not yet ACKed). // Send FIN gracefully instead of RST to avoid NS_ERROR_NET_RESET. @@ -1294,7 +1311,11 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { if (!_ssl_ctx->feedRxData((const unsigned char *)pb->payload, pb->len)) { // BIO buffer full — hold remaining pbufs without acking // LwIP backpressures naturally via TCP window - _ssl_pending_pbufs = pb; + if (_ssl_pending_pbufs) { + pbuf_chain(_ssl_pending_pbufs, pb); + } else { + _ssl_pending_pbufs = pb; + } break; } pbuf *b = pb; @@ -1337,7 +1358,11 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { _rx_last_packet = millis(); if (!_ssl_ctx->feedRxData((const unsigned char *)pb->payload, pb->len)) { // BIO buffer full — hold remaining pbufs without acking - _ssl_pending_pbufs = pb; + if (_ssl_pending_pbufs) { + pbuf_chain(_ssl_pending_pbufs, pb); + } else { + _ssl_pending_pbufs = pb; + } break; } pbuf *b = pb; @@ -1357,7 +1382,6 @@ int8_t AsyncClient::_recv(tcp_pcb *pcb, pbuf *pb, int8_t err) { async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); } } - _ssl_ctx->flushOutput(); return ERR_OK; } #endif @@ -1436,8 +1460,6 @@ int8_t AsyncClient::_poll(tcp_pcb *pcb) { async_tcp_log_elapsed("onData", _recv_cb(_recv_cb_arg, this, buf, n)); } } - _ssl_ctx->flushOutput(); - // Try to feed held pbufs now that BIO buffer has drained pbuf *pb = _ssl_pending_pbufs; _ssl_pending_pbufs = NULL; @@ -1964,6 +1986,7 @@ int8_t AsyncTCP_detail::tcp_accept(void *arg, tcp_pcb *pcb, int8_t err) { // Couldn't allocate accept event // We can't let the client object call in to close, as we're on the LWIP thread; it could deadlock trying to RPC to itself c->_pcb = nullptr; + delete c; tcp_abort(pcb); async_tcp_log_e("_accept failed: couldn't accept client"); return ERR_ABRT; diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 870572a..0f252da 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -173,14 +173,16 @@ AsyncTCPTLS::~AsyncTCPTLS() { bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { if (!_ssl_rx_buf || len == 0) return true; - // Compact: move unconsumed data to front to reclaim space - if (_ssl_rx_pos > 0) { + // Only compact when there's not enough room at the tail + size_t free_at_tail = _ssl_rx_buf_capacity - _ssl_rx_buf_len; + if (len > free_at_tail && _ssl_rx_pos > 0) { size_t remaining = _ssl_rx_buf_len - _ssl_rx_pos; if (remaining > 0) { memmove(_ssl_rx_buf, _ssl_rx_buf + _ssl_rx_pos, remaining); } _ssl_rx_buf_len = remaining; _ssl_rx_pos = 0; + free_at_tail = _ssl_rx_buf_capacity - _ssl_rx_buf_len; } // Check hard cap — if exceeded, caller must hold the pbuf @@ -189,7 +191,7 @@ bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { } // Grow buffer if needed (up to cap) - if (_ssl_rx_buf_len + len > _ssl_rx_buf_capacity) { + if (len > free_at_tail) { size_t need = _ssl_rx_buf_len + len; unsigned char *newbuf = (unsigned char *)realloc(_ssl_rx_buf, need); if (!newbuf) return false; @@ -264,10 +266,23 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return handle_error(ret); } - if (rootCABuff == NULL) { - mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); - async_tcp_log_i("WARNING: Skipping SSL Verification. INSECURE!"); - } else if (rootCABuff != NULL) { + // Force TLS 1.2 only + mbedtls_ssl_conf_min_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_max_tls_version(&ssl_conf, MBEDTLS_SSL_VERSION_TLS1_2); + + // Disable renegotiation +#if defined(MBEDTLS_SSL_RENEGOTIATION) + mbedtls_ssl_conf_renegotiation(&ssl_conf, MBEDTLS_SSL_RENEGOTIATION_DISABLED); +#endif + + // Pin fast cipher suite — hardware-accelerated AES-GCM + SHA256 on ESP32 + static const int client_ciphersuites[] = { + MBEDTLS_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + 0 + }; + mbedtls_ssl_conf_ciphersuites(&ssl_conf, client_ciphersuites); + + if (rootCABuff != NULL) { async_tcp_log_v("Loading CA cert"); mbedtls_x509_crt_init(&ca_cert); mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_REQUIRED); @@ -313,7 +328,9 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return -1; #endif } else { - return -1; + // No CA cert, no PSK — skip server verification + mbedtls_ssl_conf_authmode(&ssl_conf, MBEDTLS_SSL_VERIFY_NONE); + async_tcp_log_i("WARNING: Skipping SSL Verification. INSECURE!"); } if (rootCABuff != NULL && cli_cert != NULL && cli_key != NULL) { @@ -534,7 +551,6 @@ int AsyncTCPTLS::runSSLHandshake(void) { if (handshake_start_time == 0) handshake_start_time = millis(); ret = mbedtls_ssl_handshake(&ssl_ctx); - flushOutput(); if (ret != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { return handle_error(ret); @@ -544,15 +560,8 @@ int AsyncTCPTLS::runSSLHandshake(void) { return ret; } - // Handshake completed, validate remote side if required... - if (_have_client_cert && _have_client_key) { - async_tcp_log_d("Protocol is %s Ciphersuite is %s", mbedtls_ssl_get_version(&ssl_ctx), mbedtls_ssl_get_ciphersuite(&ssl_ctx)); - if ((ret = mbedtls_ssl_get_record_expansion(&ssl_ctx)) >= 0) { - async_tcp_log_d("Record expansion is %d", ret); - } else { - async_tcp_log_w("Record expansion is unknown (compression)"); - } - } + async_tcp_log_d("TLS handshake done: %s / %s", + mbedtls_ssl_get_version(&ssl_ctx), mbedtls_ssl_get_ciphersuite(&ssl_ctx)); async_tcp_log_v("Verifying peer X.509 certificate..."); @@ -580,7 +589,6 @@ int AsyncTCPTLS::write(const uint8_t *data, size_t len) { if (!_pcb) return -1; int ret = mbedtls_ssl_write(&ssl_ctx, data, len); - flushOutput(); if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE && ret < 0) { return handle_error(ret); } @@ -604,7 +612,6 @@ int AsyncTCPTLS::read(uint8_t *data, size_t len) { int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { if (!_pcb) return -1; int ret = mbedtls_ssl_read(&ssl_ctx, data, len); - flushOutput(); if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return 0; } @@ -623,10 +630,6 @@ int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { return ret; } -void AsyncTCPTLS::flushOutput(void) { - // No-op: tcp_output is now inlined in _tcp_ssl_write -} - void AsyncTCPTLS::logBioState(const char *tag) const { async_tcp_log_e("%s: rx_buf=%u/%u pos=%u rxBufLen=%u bytes_avail=%d", tag, (unsigned)_ssl_rx_buf_len, (unsigned)_ssl_rx_buf_capacity, @@ -637,7 +640,6 @@ void AsyncTCPTLS::logBioState(const char *tag) const { void AsyncTCPTLS::sendCloseNotify(void) { if (!_pcb) return; int ret = mbedtls_ssl_close_notify(&ssl_ctx); - flushOutput(); if (ret != 0) { async_tcp_log_d("close_notify: %d", ret); } diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 93dd2e4..ce48868 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -126,9 +126,6 @@ class AsyncTCPTLS // Decrypt application data via mbedtls_ssl_read (BIO pulls encrypted bytes internally) int sslRead(uint8_t *data, size_t len); - // Flush deferred tcp_output (called after mbedtls_ssl_handshake/write/read completes) - void flushOutput(void); - // Send TLS close_notify alert and flush to TCP void sendCloseNotify(void); }; From 5c6e79a6c1c846f06bf81859874e54abb82ad6d1 Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 29 Aug 2026 12:26:29 -0700 Subject: [PATCH 19/21] a few more fixes --- examples/ClientSSL/ClientSSL.ino | 9 +++------ src/AsyncTCPTLS.cpp | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/ClientSSL/ClientSSL.ino b/examples/ClientSSL/ClientSSL.ino index f702937..d5f21fb 100644 --- a/examples/ClientSSL/ClientSSL.ino +++ b/examples/ClientSSL/ClientSSL.ino @@ -133,11 +133,9 @@ AsyncClient *sslClient = NULL; void onConnected(void *arg, AsyncClient *client) { Serial.println("[Client] TLS handshake complete — sending request"); - const char *request = - "GET / HTTP/1.1\r\n" - "Host: " SERVER_HOST "\r\n" - "Connection: close\r\n" - "\r\n"; + char request[128]; + snprintf(request, sizeof(request), + "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", SERVER_HOST); client->write(request, strlen(request)); } @@ -149,7 +147,6 @@ void onData(void *arg, AsyncClient *client, void *data, size_t len) { void onDisconnect(void *arg, AsyncClient *client) { Serial.println("[Client] Disconnected"); - client->close(true); } void onError(void *arg, AsyncClient *client, int8_t error) { diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 0f252da..46a6ae1 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -2,6 +2,8 @@ // SSL/TLS support for AsyncTCP using mbedTLS over LwIP raw TCP (tcp_pcb) // Custom BIO callbacks replace BSD socket mbedtls_net_send/mbedtls_net_recv +#if ASYNC_TCP_SSL_ENABLED + #include #include "AsyncTCPLogging.h" #include @@ -14,7 +16,6 @@ extern "C" { #include "AsyncTCPTLS.h" -#if ASYNC_TCP_SSL_ENABLED #if !defined(MBEDTLS_KEY_EXCHANGE__SOME__PSK_ENABLED) && !defined(MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED) # warning "PSK ciphersuites not configured — PSK TLS overloads will be unavailable" #endif From 2a7cdaa7eec24ce3c52225f82de622fe737a0d28 Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 29 Aug 2026 14:51:43 -0700 Subject: [PATCH 20/21] support mbedtls 3.x and 4.x --- src/AsyncTCPTLS.cpp | 64 ++++++++++++++++++++++++++++++++++++++------- src/AsyncTCPTLS.h | 29 +++++++++++++++++--- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index 46a6ae1..d3b5855 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -111,29 +111,71 @@ static int _lwip_ssl_recv(void *ctx, unsigned char *buf, size_t len) { return sslctx->read(buf, len); } +#if ASYNCTCP_MBEDTLS_MAJOR >= 4 +// v4: RNG params removed — PSA Crypto provides the RNG internally. +int AsyncTCPTLS::_parse_private_key(mbedtls_pk_context *pk, + const unsigned char *key, size_t keylen, + const unsigned char *pwd, size_t pwdlen) { + return mbedtls_pk_parse_key(pk, key, keylen, pwd, pwdlen); +} +#else +int AsyncTCPTLS::_parse_private_key(mbedtls_pk_context *pk, + const unsigned char *key, size_t keylen, + const unsigned char *pwd, size_t pwdlen) { + return mbedtls_pk_parse_key(pk, key, keylen, pwd, pwdlen, + mbedtls_ctr_drbg_random, &drbg_ctx); +} +#endif + /* * AsyncTCPTLS implementation */ // Static shared RNG — initialized once, serialized on async task +#if ASYNCTCP_MBEDTLS_MAJOR < 4 mbedtls_ctr_drbg_context AsyncTCPTLS::drbg_ctx; mbedtls_entropy_context AsyncTCPTLS::entropy_ctx; +#endif bool AsyncTCPTLS::_conf_initialized = false; int AsyncTCPTLS::_active_count = 0; -void AsyncTCPTLS::_init_rng(void) { - if (_conf_initialized) return; +#if ASYNCTCP_MBEDTLS_MAJOR >= 4 +// Mbed TLS v4: PSA Crypto provides the RNG internally, so no app RNG is +// needed. We only have to ensure PSA is initialised (see _rng_init). +#include +#endif + +int AsyncTCPTLS::_rng_init(void) { + if (_conf_initialized) return 0; +#if ASYNCTCP_MBEDTLS_MAJOR >= 4 + psa_status_t ps = psa_crypto_init(); + if (ps != PSA_SUCCESS) { + async_tcp_log_e("psa_crypto_init failed: %d", (int)ps); + return -1; + } + _conf_initialized = true; +#else + _rng_seed_and_set(); + _conf_initialized = true; +#endif + return 0; +} + +#if ASYNCTCP_MBEDTLS_MAJOR < 4 +void AsyncTCPTLS::_rng_seed_and_set(void) { mbedtls_ctr_drbg_init(&drbg_ctx); mbedtls_entropy_init(&entropy_ctx); mbedtls_ctr_drbg_seed(&drbg_ctx, mbedtls_entropy_func, &entropy_ctx, (const unsigned char *)"AsyncTCPTLS", 11); - _conf_initialized = true; } +#endif AsyncTCPTLS::AsyncTCPTLS(void) { mbedtls_ssl_init(&ssl_ctx); mbedtls_ssl_config_init(&ssl_conf); - _init_rng(); + if (_rng_init() != 0) { + async_tcp_log_e("AsyncTCPTLS RNG init failed"); + } _pcb = NULL; _ssl_key_password = NULL; _have_ca_cert = false; @@ -351,7 +393,7 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; const unsigned char *pwd = (const unsigned char *)_ssl_key_password; size_t pwd_len = _ssl_key_password ? strlen(_ssl_key_password) : 0; - ret = mbedtls_pk_parse_key(&client_key, cli_key, cli_key_len, pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); + ret = _parse_private_key(&client_key, cli_key, cli_key_len, pwd, pwd_len); _have_client_key = true; if (ret != 0) { _deleteHandshakeCerts(); @@ -372,7 +414,9 @@ int AsyncTCPTLS::_startSSLClient(tcp_pcb *pcb, const char *host_or_ip, return handle_error(ret); } +#if ASYNCTCP_MBEDTLS_MAJOR < 4 mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); +#endif // Reduce buffer sizes to fit ESP32 heap (requires MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) #if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) @@ -507,16 +551,16 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, (const unsigned char *)pem_buf, pwd, pwd_len, &use_len); if (ret == 0) { const unsigned char *der_buf = mbedtls_pem_get_buffer(&pem, &key_der_len); - ret = mbedtls_pk_parse_key(&client_key, der_buf, key_der_len, - pwd, pwd_len, mbedtls_ctr_drbg_random, &drbg_ctx); + ret = _parse_private_key(&client_key, der_buf, key_der_len, + pwd, pwd_len); } mbedtls_pem_free(&pem); free(pem_buf); } else { // DER — parse directly with length async_tcp_log_v("Parsing key DER (%u bytes)", (unsigned)server_key_len); - ret = mbedtls_pk_parse_key(&client_key, server_key, server_key_len, - NULL, 0, mbedtls_ctr_drbg_random, &drbg_ctx); + ret = _parse_private_key(&client_key, server_key, server_key_len, + NULL, 0); } _have_client_key = true; if (ret != 0) { @@ -526,7 +570,9 @@ int AsyncTCPTLS::startSSLServer(tcp_pcb *pcb, mbedtls_ssl_conf_own_cert(&ssl_conf, &client_cert, &client_key); +#if ASYNCTCP_MBEDTLS_MAJOR < 4 mbedtls_ssl_conf_rng(&ssl_conf, mbedtls_ctr_drbg_random, &drbg_ctx); +#endif #if defined(MBEDTLS_SSL_MAX_FRAGMENT_LENGTH) mbedtls_ssl_conf_max_frag_len(&ssl_conf, MBEDTLS_SSL_MAX_FRAG_LEN_4096); diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index ce48868..1959bb7 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -2,11 +2,24 @@ #if ASYNC_TCP_SSL_ENABLED +// --- mbedTLS version abstraction ----------------------------------------- +// Mbed TLS v3 (stable Arduino core 3.x): legacy entropy + CTR-DRBG RNG. +// Mbed TLS v4 (dev core / ESP-IDF 6.0): PSA Crypto; entropy/ctr_drbg headers +// and app-supplied RNG callbacks were removed. +#include "mbedtls/build_info.h" // defines MBEDTLS_VERSION_MAJOR on v3/v4 +#ifndef MBEDTLS_VERSION_MAJOR +#define MBEDTLS_VERSION_MAJOR 3 +#endif +#define ASYNCTCP_MBEDTLS_MAJOR MBEDTLS_VERSION_MAJOR +// ------------------------------------------------------------------------- + #include "mbedtls/platform.h" #include "mbedtls/debug.h" #include "mbedtls/ssl.h" +#if ASYNCTCP_MBEDTLS_MAJOR < 4 #include "mbedtls/entropy.h" #include "mbedtls/ctr_drbg.h" +#endif #include "mbedtls/error.h" #include "mbedtls/pem.h" #include "mbedtls/sha256.h" @@ -34,16 +47,26 @@ class AsyncTCPTLS mbedtls_ssl_context ssl_ctx; mbedtls_ssl_config ssl_conf; - // Shared across all instances — initialized once, RNG is serialized on async task + // Shared RNG — initialized once, serialized on async task. + // v3: legacy entropy + CTR-DRBG. v4: PSA Crypto (no DRBG types). +#if ASYNCTCP_MBEDTLS_MAJOR < 4 static mbedtls_ctr_drbg_context drbg_ctx; static mbedtls_entropy_context entropy_ctx; +#endif static bool _conf_initialized; + static int _rng_init(void); +#if ASYNCTCP_MBEDTLS_MAJOR < 4 + static void _rng_seed_and_set(void); +#endif + // Parse a PEM/DER private key. v3 supplies the legacy RNG; v4 uses PSA. + static int _parse_private_key(mbedtls_pk_context *pk, + const unsigned char *key, size_t keylen, + const unsigned char *pwd, size_t pwdlen); + // Concurrent connection tracking static int _active_count; - static void _init_rng(void); - mbedtls_x509_crt ca_cert; mbedtls_x509_crt client_cert; mbedtls_pk_context client_key; From bd89f2e09285b42ce81219c2e1751ca90c4d7f18 Mon Sep 17 00:00:00 2001 From: Dima Date: Sat, 29 Aug 2026 16:11:19 -0700 Subject: [PATCH 21/21] compiler cleanup --- src/AsyncTCPTLS.cpp | 2 -- src/AsyncTCPTLS.h | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp index d3b5855..441375e 100644 --- a/src/AsyncTCPTLS.cpp +++ b/src/AsyncTCPTLS.cpp @@ -6,8 +6,6 @@ #include #include "AsyncTCPLogging.h" -#include -#include #include extern "C" { diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h index 1959bb7..a6910d0 100644 --- a/src/AsyncTCPTLS.h +++ b/src/AsyncTCPTLS.h @@ -22,8 +22,6 @@ #endif #include "mbedtls/error.h" #include "mbedtls/pem.h" -#include "mbedtls/sha256.h" -#include "mbedtls/oid.h" struct tcp_pcb;