diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84266b5..a04992e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,12 @@ 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 + + - 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 @@ -101,6 +107,10 @@ 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 + - 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..d5f21fb --- /dev/null +++ b/examples/ClientSSL/ClientSSL.ino @@ -0,0 +1,202 @@ +/* + * 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 = "ESP32-AP"; +const char *PASSWORD = "12345678"; + +// -- Server to connect to ------------------------------------------- +const char *SERVER_HOST = "192.168.4.1"; +const uint16_t SERVER_PORT = 443; + +// -- 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"); + + 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)); +} + +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"); +} + +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 new file mode 100644 index 0000000..82a3801 --- /dev/null +++ b/examples/WebServerSSL/WebServerSSL.ino @@ -0,0 +1,185 @@ +/* + * AsyncTCP - WebServerSSL example + * + * ESP32 HTTPS server using AsyncTCP + AsyncTCPTLS directly. + * Serves a simple page and a JSON API over TLS 1.2 on port 443. + * Uses a CA-signed cert — the same CA that signs the ClientSSL example's + * client cert, so either example can verify the other. + * + * 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 + * + * Compile with: + * compiler.cpp.extra_flags=-DASYNC_TCP_SSL_ENABLED=1 + */ + +#include +#include + +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) +static const char SERVER_CERT[] PROGMEM = R"EOF( +-----BEGIN CERTIFICATE----- +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----- +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"; + +AsyncServer *sslServer = NULL; + +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( + + + + + WebServerSSL + + + +

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); + + 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); + sslServer->beginSecure(SERVER_CERT, SERVER_KEY, "test123"); + + Serial.printf("[Server] HTTPS on port 443\n"); + Serial.printf("Test: curl -k https://%s/api\n", + WiFi.softAPIP().toString().c_str()); +} + +void loop() { + delay(10000); +} 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 5a172e1..8ca9290 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 "AsyncTCPTLS.h" +#endif + /** * LibreTiny specific configurations */ @@ -412,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) { @@ -652,9 +669,14 @@ 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) { - // 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 @@ -777,6 +799,19 @@ 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; + _ssl_key_password = NULL; + _ssl_pending_pbufs = NULL; +#endif if (_pcb) { _rx_last_packet = millis(); _bind_tcp_callbacks(_pcb, this); @@ -784,6 +819,17 @@ 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; + } + if (_ssl_key_password) { ::free((void*)_ssl_key_password); _ssl_key_password = NULL; } +#endif if (_pcb) { _close(); } @@ -937,6 +983,84 @@ bool AsyncClient::connect(const char *host, uint16_t port) { return false; } +#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; + 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, + 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, + 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 char *keyPassword) { + 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; + if (_ssl_key_password) { ::free((void*)_ssl_key_password); _ssl_key_password = NULL; } + _ssl_key_password = keyPassword ? strdup(keyPassword) : NULL; + return connect(host, port); +} + +void AsyncClient::feedSSLRxData(const unsigned char *data, size_t len) { + if (_ssl_ctx) { + _ssl_ctx->feedRxData(data, len); + } +} + +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->sslRead(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 +1091,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 +1149,21 @@ 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; + } + _clearSSLParams(); +#endif int8_t err = _tcp_close(&_pcb, this); // _pcb is now NULL if ((err == ERR_OK) && _discard_cb) { @@ -1035,6 +1184,68 @@ 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) AsyncTCPTLS(); + 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; + 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; + _ssl_ctx = 0; + _clearSSLParams(); + 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); + _ssl_ctx->logBioState("handshake_poll"); + _clearSSLParams(); + 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 +1277,17 @@ 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; + _ssl_handshake_done = false; + } +#endif close(); return ERR_OK; } @@ -1079,6 +1301,92 @@ 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: 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 + if (_ssl_pending_pbufs) { + pbuf_chain(_ssl_pending_pbufs, pb); + } else { + _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); + } + // 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); + _ssl_ctx->logBioState("handshake"); + _clearSSLParams(); + 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: 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 + if (_ssl_pending_pbufs) { + pbuf_chain(_ssl_pending_pbufs, pb); + } else { + _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); + } + // Decrypt all available plaintext + 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)); + } + } + 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 +1423,66 @@ 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; + } + + // 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)); + } + } + // 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 if (_ack_timeout) { const uint32_t one_day = 86400000; @@ -1168,6 +1536,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 +1820,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 +1848,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 +1872,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 +1886,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; @@ -1590,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; @@ -1611,6 +2008,41 @@ 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); + 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(); + delete client; + return ERR_ABRT; + } + } else { + async_tcp_log_e("Failed to allocate SSL context for server"); + client->abort(); + delete client; + return ERR_ABRT; + } + } +#endif if (_connect_cb) { async_tcp_log_elapsed("onClient", _connect_cb(_connect_cb_arg, client)); } @@ -1625,6 +2057,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..79cebde 100644 --- a/src/AsyncTCP.h +++ b/src/AsyncTCP.h @@ -7,6 +7,11 @@ #include "AsyncTCPVersion.h" #define ASYNCTCP_FORK_ESP32Async +// 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 + #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,10 @@ 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 +#include "AsyncTCPTLS.h" +#endif + typedef std::function AcConnectHandler; typedef std::function AcAckHandler; typedef std::function AcErrorHandler; @@ -268,6 +281,27 @@ class AsyncClient { return _pcb; } +#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 *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 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; } + AsyncTCPTLS *getSSLContext() { return _ssl_ctx; } + void feedSSLRxData(const unsigned char *data, size_t len); + 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,22 @@ 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 + AsyncTCPTLS *_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; + char *_ssl_key_password; + void _clearSSLParams(void); + pbuf *_ssl_pending_pbufs; +#endif }; class AsyncServer { @@ -328,6 +378,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 +401,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/AsyncTCPTLS.cpp b/src/AsyncTCPTLS.cpp new file mode 100644 index 0000000..441375e --- /dev/null +++ b/src/AsyncTCPTLS.cpp @@ -0,0 +1,713 @@ +// 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 + +#if ASYNC_TCP_SSL_ENABLED + +#include +#include "AsyncTCPLogging.h" +#include + +extern "C" { +#include "lwip/tcp.h" +} + +#include "AsyncTCPTLS.h" + +#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 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 + +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); + 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) + +#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); + if (msg->err == ERR_OK) { + 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; +} + +/* + * Custom LwIP BIO callbacks for mbedTLS + * These bridge mbedTLS's I/O with LwIP raw TCP (tcp_pcb). + * The void* ctx points to the AsyncTCPTLS instance. + */ + +static int _lwip_ssl_send(void *ctx, const unsigned char *buf, size_t len) { + AsyncTCPTLS *sslctx = (AsyncTCPTLS *)ctx; + if (!sslctx || !sslctx->pcb()) { + return MBEDTLS_ERR_NET_SEND_FAILED; + } + tcp_pcb *pcb = sslctx->pcb(); + + err_t err = _tcp_ssl_write(pcb, buf, len, TCP_WRITE_FLAG_COPY); + if (err == ERR_OK) { + 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) { + AsyncTCPTLS *sslctx = (AsyncTCPTLS *)ctx; + if (!sslctx || !sslctx->hasRxData()) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + 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; + +#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); +} +#endif + +AsyncTCPTLS::AsyncTCPTLS(void) { + mbedtls_ssl_init(&ssl_ctx); + mbedtls_ssl_config_init(&ssl_conf); + if (_rng_init() != 0) { + async_tcp_log_e("AsyncTCPTLS RNG init failed"); + } + _pcb = NULL; + _ssl_key_password = NULL; + _have_ca_cert = false; + _have_client_cert = false; + _have_client_key = false; + 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() { + _deleteHandshakeCerts(); + + async_tcp_log_v("~AsyncTCPTLS"); + + mbedtls_ssl_free(&ssl_ctx); + mbedtls_ssl_config_free(&ssl_conf); + + if (_ssl_key_password) { + free((void*)_ssl_key_password); + _ssl_key_password = NULL; + } + + if (_ssl_rx_buf) { + free(_ssl_rx_buf); + _ssl_rx_buf = NULL; + } + + _active_count--; +} + +bool AsyncTCPTLS::feedRxData(const unsigned char *data, size_t len) { + if (!_ssl_rx_buf || len == 0) return true; + + // 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 + if (_ssl_rx_buf_len + len > ASYNCTCP_TLS_RX_BUF_MAX) { + return false; + } + + // Grow buffer if needed (up to cap) + 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; + _ssl_rx_buf = newbuf; + _ssl_rx_buf_capacity = need; + } + memcpy(_ssl_rx_buf + _ssl_rx_buf_len, data, len); + _ssl_rx_buf_len += len; + return true; +} + +int AsyncTCPTLS::startSSLClientInsecure(tcp_pcb *pcb, const char *host_or_ip) { + return _startSSLClient(pcb, host_or_ip, + NULL, 0, + NULL, 0, + NULL, 0, + NULL, NULL); +} + +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, + NULL, 0, + NULL, 0, + 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 *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, + 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 char *keyPassword) { + return _startSSLClient(pcb, host_or_ip, + rootCABuff, rootCABuff_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len, + NULL, NULL, + 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 char *pskIdent, const char *psKey, + const char *keyPassword) { + int ret; + + if (!pcb) { + return -1; + } + + 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); + } + + // 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); + 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) { +#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"); + 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)); + mbedtls_platform_zeroize(psk, sizeof(psk)); + if (ret != 0) { + 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 { + // 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) { + 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"); + 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; + ret = _parse_private_key(&client_key, cli_key, cli_key_len, pwd, pwd_len); + _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 (!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); + } + +#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) + 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(); + 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 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) { + int ret; + + if (server_cert == NULL || server_key == NULL || !pcb) { + return -1; + } + + 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)"); + + 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 (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[] = { + 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 + mbedtls_x509_crt_init(&client_cert); + + 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-----", + (const unsigned char *)pem_buf, NULL, 0, &use_len); + if (ret == 0) { + 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) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + // Load server private key + mbedtls_pk_init(&client_key); + + 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(pem_buf, "-----BEGIN ENCRYPTED PRIVATE KEY-----", 37) == 0) { + key_header = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + key_footer = "-----END ENCRYPTED PRIVATE KEY-----"; + } else if (strncmp(pem_buf, "-----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, + (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 = _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 = _parse_private_key(&client_key, server_key, server_key_len, + NULL, 0); + } + _have_client_key = true; + if (ret != 0) { + _deleteHandshakeCerts(); + return handle_error(ret); + } + + 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); +#endif + + 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; + handshake_timeout = SSL_HANDSHAKE_TIMEOUT; + + return 0; +} + +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); + 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; + } + + 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..."); + + 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); + return handle_error(-1); + } + } else { + async_tcp_log_v("Certificate verified."); + } + + async_tcp_log_v("Free internal heap after TLS %u", ESP.getFreeHeap()); + + return 0; +} + +int AsyncTCPTLS::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 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; + 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; + } + return (int)copy; +} + +int AsyncTCPTLS::sslRead(uint8_t *data, size_t len) { + if (!_pcb) return -1; + int ret = mbedtls_ssl_read(&ssl_ctx, data, len); + 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); + logBioState("sslRead"); + return -1; + } + return ret; +} + +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); + 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; + } + if (_have_client_key) { + async_tcp_log_v("Cleaning client certificate key."); + mbedtls_pk_free(&client_key); + _have_client_key = false; + } +} + +#endif // ASYNC_TCP_SSL_ENABLED diff --git a/src/AsyncTCPTLS.h b/src/AsyncTCPTLS.h new file mode 100644 index 0000000..a6910d0 --- /dev/null +++ b/src/AsyncTCPTLS.h @@ -0,0 +1,154 @@ +#pragma once + +#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" + +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_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 +{ +private: + mbedtls_ssl_context ssl_ctx; + mbedtls_ssl_config ssl_conf; + + // 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; + + 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 (owned copy via strdup) + char *_ssl_key_password; + + // 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; + + 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, + const char *keyPassword = NULL); + + void _deleteHandshakeCerts(void); + +public: + AsyncTCPTLS(void); + virtual ~AsyncTCPTLS(); + + // 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; } + + // 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; + + static int getActiveCount() { return _active_count; } + + 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, + const char *cli_cert, + 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 char *keyPassword = NULL); + + 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); + + // Decrypt application data via mbedtls_ssl_read (BIO pulls encrypted bytes internally) + int sslRead(uint8_t *data, size_t len); + + // Send TLS close_notify alert and flush to TCP + void sendCloseNotify(void); +}; + +#endif // ASYNC_TCP_SSL_ENABLED