#include "configuration.h" #if HAS_ETHERNET && defined(HAS_ETHERNET_TLS_API) && defined(ARCH_RP2040) #include "concurrency/OSThread.h" #include "ethApiHandlers.h" #include "ethCert.h" #include "ethTlsApiServer.h" #include #ifdef USE_ARDUINO_ETHERNET #include #else #include #endif #include #include #include #include #include #include #ifndef ETH_TLS_API_PORT #define ETH_TLS_API_PORT 443 #endif // Adaptive poll intervals (mirror ethApiServer.cpp). static constexpr uint32_t ACTIVE_THRESHOLD_MS = 5000; static constexpr uint32_t MEDIUM_THRESHOLD_MS = 30000; static constexpr int32_t ACTIVE_INTERVAL_MS = 20; static constexpr int32_t MEDIUM_INTERVAL_MS = 100; static constexpr int32_t IDLE_INTERVAL_MS = 500; // Matches the keep-alive idle window in ethApiHandlers - if the handler // loop calls read() and netRecv blocked for 10 s, the 3 s idle deadline // inside parseRequest would be irrelevant and the OSThread would stay stuck // long after a quiet browser closed its end of the TCP socket. static constexpr uint32_t RECV_TIMEOUT_MS = 3000; // Reuse the picoRand callback semantics from ethCert.cpp. Local copy so we // don't have to expose it through a header; the cost is two trivial functions. static int picoRand(void * /*ctx*/, unsigned char *out, size_t len) { while (len > 0) { uint64_t r = get_rand_64(); size_t to_copy = len > sizeof(r) ? sizeof(r) : len; memcpy(out, &r, to_copy); out += to_copy; len -= to_copy; } return 0; } // One-shot TLS context lives in BSS - keeps mbedtls allocations off the // OSThread stack (lesson from Phase 2.1-bis: stack budget is tight on M33). static EthernetServer *tlsServer = nullptr; static mbedtls_x509_crt certChain; static mbedtls_pk_context pkKey; static mbedtls_ssl_config sslConf; static mbedtls_ssl_context ssl; static bool tlsReady = false; // Adapter: route mbedtls_ssl_set_bio() through the EthernetClient instance // that runOnce() is currently servicing. The void* ctx we hand mbedtls is a // pointer to the EthernetClient. static int netSend(void *ctx, const unsigned char *buf, size_t len) { auto *client = static_cast(ctx); if (!client->connected()) return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; // Block-with-yield until the W5500 TX buffer can absorb the chunk. // Returning WANT_WRITE without delay made mbedtls_ssl_handshake() spin // at ~180k iter/s when Chrome was slow to drain the socket during the // ECDHE-ECDSA ServerKeyExchange - the original code logged exactly that // signature (ret=-0x6880 / WANT_WRITE) tight-looping forever. Firefox // happened to read fast enough that the buffer never filled. uint32_t t0 = millis(); while (true) { if (!client->connected()) return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; size_t w = client->write(buf, len); if (w > 0) return (int)w; if (millis() - t0 > RECV_TIMEOUT_MS) return MBEDTLS_ERR_SSL_TIMEOUT; watchdog_update(); delay(2); } } static int netRecv(void *ctx, unsigned char *buf, size_t len) { auto *client = static_cast(ctx); // Block-with-timeout: spin until bytes arrive, the peer closes, or we // exceed the per-recv budget. Pure non-blocking (return WANT_READ) would // require mbedtls_ssl_handshake to be driven from the runOnce dispatcher // - overkill for the Phase 2.2 skeleton with a single in-flight session. // // Pet the 8 s hardware watchdog from inside the poll loop. We sit here // for up to RECV_TIMEOUT_MS waiting for the next keep-alive request, and // a quiet client can string two such waits back-to-back (6 s) plus the // earlier handshake/handler time - easily past the watchdog deadline. // The main loop()'s watchdog_update() never runs while the OSThread is // inside serveClient(), so it has to be done here. uint32_t t0 = millis(); while (client->available() == 0) { if (!client->connected()) return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; if (millis() - t0 > RECV_TIMEOUT_MS) return MBEDTLS_ERR_SSL_TIMEOUT; watchdog_update(); delay(2); } int n = client->read(buf, len); if (n <= 0) return MBEDTLS_ERR_SSL_WANT_READ; return n; } // Bridges mbedtls_ssl_read/write to the request handlers via the same // IStreamReadWrite interface the plain-HTTP server uses. class MbedTlsStream : public IStreamReadWrite { public: MbedTlsStream(mbedtls_ssl_context *s, EthernetClient *c) : ssl_(s), client_(c) {} size_t write(uint8_t b) override { int r = mbedtls_ssl_write(ssl_, &b, 1); return r > 0 ? 1 : 0; } size_t write(const uint8_t *buf, size_t len) override { size_t total = 0; while (total < len) { int r = mbedtls_ssl_write(ssl_, buf + total, len - total); if (r == MBEDTLS_ERR_SSL_WANT_WRITE || r == MBEDTLS_ERR_SSL_WANT_READ) continue; if (r <= 0) break; total += (size_t)r; } return total; } int available() override { // mbedtls buffers internally; ssl_get_bytes_avail reports what is // already decoded. If 0, peek a record via read into a 1-byte buffer. size_t pending = mbedtls_ssl_get_bytes_avail(ssl_); if (pending > 0) return (int)pending; // Best-effort: report network bytes (rough proxy - handlers usually // call read() in a loop and tolerate slow streams). return client_->available(); } int read() override { uint8_t b; int r = mbedtls_ssl_read(ssl_, &b, 1); return r == 1 ? (int)b : -1; } int read(uint8_t *buf, size_t len) override { int r = mbedtls_ssl_read(ssl_, buf, len); return r > 0 ? r : -1; } bool connected() override { return client_->connected(); } void flush() override { client_->flush(); } IPAddress remoteIP() override { return client_->remoteIP(); } private: mbedtls_ssl_context *ssl_; EthernetClient *client_; }; class EthTlsApiServerThread : public concurrency::OSThread { public: EthTlsApiServerThread() : concurrency::OSThread("EthTlsApi") { lastActivityMs = millis(); } protected: int32_t runOnce() override { // Phase A: wait for the cert worker, then rebuild if the cert was // regenerated (a DHCP lease change to a new IP bumps the generation, and // the SAN must follow or browsers reject the new address). if (tlsReady && getEthCertGeneration() != loadedCertGen_) { LOG_INFO("ETH TLS: cert regenerated (gen %u->%u), reloading + rebinding", (unsigned)loadedCertGen_, (unsigned)getEthCertGeneration()); deInitEthTlsApiServer(); // frees ctx, drops the listener, clears tlsReady } if (!tlsReady) { if (!isEthCertReady()) return 500; if (!initTlsContext()) return INT32_MAX; // hard fail - TLS server stays disabled loadedCertGen_ = getEthCertGeneration(); tlsReady = true; } // Phase B: accept + serve one client. if (!tlsServer) return INT32_MAX; EthernetClient client = tlsServer->accept(); if (client) { lastActivityMs = millis(); serveClient(client); client.stop(); } uint32_t since = millis() - lastActivityMs; if (since < ACTIVE_THRESHOLD_MS) return ACTIVE_INTERVAL_MS; if (since < MEDIUM_THRESHOLD_MS) return MEDIUM_INTERVAL_MS; return IDLE_INTERVAL_MS; } private: uint32_t lastActivityMs; uint32_t loadedCertGen_ = 0; // cert generation the current TLS context was built from bool initTlsContext() { const EthCertMaterial &cert = getEthCert(); if (cert.certDer.empty() || cert.keyDer.empty()) { LOG_ERROR("ETH TLS: cert material is empty, refusing to start TLS server"); return false; } mbedtls_x509_crt_init(&certChain); mbedtls_pk_init(&pkKey); mbedtls_ssl_config_init(&sslConf); mbedtls_ssl_init(&ssl); int ret; ret = mbedtls_x509_crt_parse_der(&certChain, cert.certDer.data(), cert.certDer.size()); if (ret != 0) { LOG_ERROR("ETH TLS: x509_crt_parse_der failed -0x%04x", -ret); return false; } ret = mbedtls_pk_parse_key(&pkKey, cert.keyDer.data(), cert.keyDer.size(), nullptr, 0, picoRand, nullptr); if (ret != 0) { LOG_ERROR("ETH TLS: pk_parse_key failed -0x%04x", -ret); return false; } ret = mbedtls_ssl_config_defaults(&sslConf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { LOG_ERROR("ETH TLS: ssl_config_defaults failed -0x%04x", -ret); return false; } mbedtls_ssl_conf_rng(&sslConf, picoRand, nullptr); mbedtls_ssl_conf_authmode(&sslConf, MBEDTLS_SSL_VERIFY_NONE); // TLS 1.3 is compiled out via mbedtls_user_config.h (it would crash // on Chrome's ClientHello extensions). These calls pin runtime to // 1.2 too as a defense-in-depth: if a future user config flip // re-enables 1.3 code, the config layer still won't negotiate it. mbedtls_ssl_conf_max_tls_version(&sslConf, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_min_tls_version(&sslConf, MBEDTLS_SSL_VERSION_TLS1_2); ret = mbedtls_ssl_conf_own_cert(&sslConf, &certChain, &pkKey); if (ret != 0) { LOG_ERROR("ETH TLS: conf_own_cert failed -0x%04x", -ret); return false; } ret = mbedtls_ssl_setup(&ssl, &sslConf); if (ret != 0) { LOG_ERROR("ETH TLS: ssl_setup failed -0x%04x", -ret); return false; } tlsServer = new EthernetServer(ETH_TLS_API_PORT); tlsServer->begin(); LOG_INFO("ETH TLS: server listening on TCP port %d", ETH_TLS_API_PORT); return true; } void serveClient(EthernetClient &client) { LOG_INFO("ETH TLS: client connected from %s", client.remoteIP().toString().c_str()); mbedtls_ssl_session_reset(&ssl); mbedtls_ssl_set_bio(&ssl, &client, netSend, netRecv, nullptr); uint32_t t0 = millis(); int ret; do { ret = mbedtls_ssl_handshake(&ssl); watchdog_update(); } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); if (ret != 0) { char err[80]; mbedtls_strerror(ret, err, sizeof(err)); LOG_WARN("ETH TLS: handshake failed -0x%04x (%s) after %u ms", -ret, err, (unsigned)(millis() - t0)); return; } LOG_INFO("ETH TLS: handshake OK in %u ms, ciphersuite=%s", (unsigned)(millis() - t0), mbedtls_ssl_get_ciphersuite(&ssl)); MbedTlsStream stream(&ssl, &client); handleApiClient(stream); mbedtls_ssl_close_notify(&ssl); } }; static EthTlsApiServerThread *tlsThread = nullptr; void initEthTlsApiServer() { if (tlsThread) return; tlsThread = new EthTlsApiServerThread(); LOG_INFO("ETH TLS: server worker scheduled (waits for cert ready)"); } void deInitEthTlsApiServer() { // A W5500 chip reset leaves tlsServer bound to a dead socket and the cached // mbedTLS context stale. Reset the worker back to Phase A (free the context, // drop the listener, clear tlsReady) WITHOUT deleting the OSThread - its next // runOnce re-waits for isEthCertReady() and rebuilds the context + rebinds // TCP/443. Safe to free here: this runs in reconnectETH (ethConnect thread), // and the cooperative scheduler guarantees tlsThread is not mid-runOnce, so // nothing is using these contexts right now. if (tlsServer) { delete tlsServer; tlsServer = nullptr; } if (tlsReady) { mbedtls_ssl_free(&ssl); mbedtls_ssl_config_free(&sslConf); mbedtls_pk_free(&pkKey); mbedtls_x509_crt_free(&certChain); tlsReady = false; } } #endif // HAS_ETHERNET && HAS_ETHERNET_TLS_API && ARCH_RP2040