To get HTTPS working on an ESP32 Dev Module, you must use the WiFiClientSecure library, provide a valid SHA-256 root certificate (or fingerprint), and ensure the device's internal RTC is synced via NTP so the certificate's validity dates pass verification. If your ESP32 is throwing handshake errors, the root cause is almost always an unsynced clock, an expired root CA, or heap memory fragmentation during the mbedTLS handshake.
This guide provides a complete, production-ready hardware setup, fully compilable Arduino C++ code with robust error handling, and a debugging matrix for the most common SSL failures encountered on the bench.
Project Specs and Hardware Setup
Estimated Build Time: 20 minutes (Hardware) + 15 minutes (Software/Debugging)
Target Board Variant: Espressif ESP32 DevKit V1 (ESP32-WROOM-32, 30-pin or 38-pin)
The ESP32-WROOM-32 is the standard workhorse for IoT projects. While the chip has built-in WiFi, it does not have a hardware cryptographic accelerator for TLS 1.2/1.3. Instead, it relies on the software-based mbedTLS library, which demands significant RAM during the handshake phase.
Parts List
- Microcontroller: ESP32 DevKit V1 (ESP32-WROOM-32 module)
- Status Indicator: 5mm Red LED (or use the built-in blue LED on GPIO 2)
- Current Limiting: 330Ω resistor (1/4W)
- Trigger Input: 6x6mm tactile pushbutton
- Prototyping: Half-size breadboard and male-to-male jumper wires
- Power: USB-C or Micro-USB cable (data-capable) connected to a 5V/1A+ source
Pin Mapping for the ESP32 DevKit V1
While WiFi operations are internal, a robust embedded project requires physical feedback mechanisms. We are mapping an external status LED and a hardware trigger button to avoid relying solely on the Serial Monitor.
| Component | ESP32 GPIO | Notes & Constraints |
|---|---|---|
| Built-in LED | GPIO 2 | Active HIGH. Also tied to boot mode; keep LOW on reset. |
| External Status LED | GPIO 15 | Connect anode to GPIO 15, cathode to 330Ω resistor, then GND. |
| Trigger Button | GPIO 0 | Active LOW. Shared with BOOT button on most DevKits. Use internal pull-up. |
Compilable HTTPS Client Code
The following code targets the Arduino ESP32 Core v2.x or v3.x. It connects to WiFi, syncs the RTC via NTP (critical for certificate validation), and performs an HTTPS GET request to a test endpoint. It includes the ISRG Root X1 certificate, which is the standard root for Let's Encrypt and many modern CDNs.
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <time.h>
// --- PIN DEFINITIONS ---
const int PIN_BUILTIN_LED = 2;
const int PIN_EXT_LED = 15;
const int PIN_TRIGGER_BTN = 0;
// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// --- TARGET ENDPOINT ---
const char* target_url = "https://jsonplaceholder.typicode.com/todos/1";
// --- ROOT CERTIFICATE (ISRG Root X1) ---
const char* root_ca = R"(-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4xKKIVfbOMenFi1vZ1n3
l+U1xwX5b9G8bX3vY7Z8a7V7b7W8a7X7z7Y7a7b7c7d7e7f7g7h7i7j7k7l7m7n7o
7p7q7r7s7t7u7v7w7x7y7z7a7b7c7d7e7f7g7h7i7j7k7l7m7n7o7p7q7r7s7t7u
7v7w7x7y7z7A7B7C7D7E7F7G7H7I7J7K7L7M7N7O7P7Q7R7S7T7U7V7W7X7Y7Z
-----END CERTIFICATE-----)";
// Note: The above cert string is truncated for display.
// In production, use the full 1900+ character ISRG Root X1 PEM.
WiFiClientSecure client;
HTTPClient https;
void setup() {
Serial.begin(115200);
pinMode(PIN_BUILTIN_LED, OUTPUT);
pinMode(PIN_EXT_LED, OUTPUT);
pinMode(PIN_TRIGGER_BTN, INPUT_PULLUP);
digitalWrite(PIN_BUILTIN_LED, LOW);
digitalWrite(PIN_EXT_LED, LOW);
// Connect to WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected!");
digitalWrite(PIN_BUILTIN_LED, HIGH);
// CRITICAL: Sync time via NTP for certificate validation
Serial.print("Syncing NTP time...");
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t now = time(nullptr);
while (now < 8 * 3600 * 2) { // Wait for valid timestamp
delay(500);
Serial.print(".");
now = time(nullptr);
}
Serial.println(" Time synced.");
// Set the root CA for the secure client
client.setCACert(root_ca);
}
void loop() {
// Trigger HTTPS request when button is pressed (Active LOW)
if (digitalRead(PIN_TRIGGER_BTN) == LOW) {
Serial.println("\nButton pressed. Initiating HTTPS GET...");
digitalWrite(PIN_EXT_LED, HIGH);
if (WiFi.status() == WL_CONNECTED) {
https.begin(client, target_url);
int httpCode = https.GET();
if (httpCode > 0) {
Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
String payload = https.getString();
Serial.println("Payload:");
Serial.println(payload);
}
} else {
Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
}
https.end();
}
digitalWrite(PIN_EXT_LED, LOW);
delay(500); // Simple debounce
}
}
Debugging: "Handshake Failed" and SSL Errors
When implementing HTTPS on ESP32, the Serial Monitor will inevitably throw SSL errors. The mbedTLS library is notoriously verbose, but the errors map to specific failure modes. If your connection drops, here are the first three things to check:
- NTP Time Sync: If the ESP32 thinks the year is 1970, any certificate with a
notBeforedate of 2015 or later will be rejected as "not yet valid." Check yourconfigTime()execution. - Root CA Rotation: Servers rotate certificates. If you hardcoded a DST Root CA X3 (expired Sept 2021) or an older DigiCert root, the chain will fail. Always verify the current root CA via a browser's lock icon.
- Heap Fragmentation: The TLS handshake requires a contiguous block of ~35KB to 45KB of RAM. If your heap is fragmented by String manipulations, the allocation fails silently or throws a memory error.
Common Error Strings and Ranked Causes
| Exact Error String | Ranked Causes (Most to Least Likely) | Fix |
|---|---|---|
WiFiClientSecure: handshake failed |
1. Expired/Wrong Root CA 2. NTP not synced 3. Server requires TLS 1.3 (ESP32 defaults to 1.2) |
Update root_ca string; verify NTP; check server TLS config. |
mbedtls_ssl_handshake returned -0x2700 |
1. Heap memory exhaustion 2. Network dropped during handshake |
Print ESP.getFreeHeap(). Reboot ESP32 or optimize memory usage. |
X509 - Certificate verification failed |
1. Hostname mismatch (URL vs Cert CN) 2. Intermediate cert missing in chain |
Ensure URL exactly matches the domain on the cert. Use full chain. |
Extending and Simplifying the Build
Depending on your deployment environment, you may need to scale this project up for production or strip it down for rapid prototyping.
How to Simplify (Development Phase Only)
If you are testing against a local server with a self-signed certificate, or you just want to bypass the headache of root CAs during early development, you can disable certificate validation entirely. Add client.setInsecure(); immediately after initializing the WiFiClientSecure object, and remove the client.setCACert(root_ca); line.
setInsecure() encrypts the payload but completely defeats identity verification. Your ESP32 is now vulnerable to Man-In-The-Middle (MITM) attacks. Never use this in a production firmware image that handles credentials or PII.
How to Extend (Production Phase)
- POST Requests with JSON: To send data, use
https.addHeader("Content-Type", "application/json");and pass your JSON string tohttps.POST(payload). Use theArduinoJsonlibrary to serialize C++ structs into JSON strings efficiently, avoiding the heap fragmentation caused by the nativeStringclass. - Connection Keep-Alive: TLS handshakes take 1-3 seconds and burn CPU cycles. If polling an API every 5 seconds, reuse the connection by keeping the
HTTPClientobject alive and checkinghttps.connected()before initiating a new request. - Fingerprint Validation: If RAM is severely constrained and you cannot afford the 2KB+ overhead of a full PEM root certificate, you can validate the server using a 20-byte SHA-1 fingerprint via
client.setFingerprint(). Note that SHA-1 is deprecated for root CAs but still widely used for endpoint fingerprinting in constrained IoT devices.
Frequently Asked Questions
How do I bypass certificate validation for HTTPS on ESP32?
Call client.setInsecure(); on your WiFiClientSecure instance before passing it to the HTTPClient. This skips the mbedTLS X509 verification step. While it saves roughly 10KB of RAM and eliminates the need for NTP time syncing, it exposes your device to MITM attacks. Only use this for local network testing or when connecting to legacy hardware that cannot serve valid certificates.
Why does my ESP32 run out of memory during HTTPS requests?
The mbedTLS library requires a contiguous block of memory (often 35KB to 45KB) to perform the cryptographic handshake. If your sketch heavily uses the Arduino String class, the heap becomes fragmented, leaving plenty of total free RAM but no single contiguous block large enough for TLS. Switch to fixed-size char arrays or use std::vector with pre-allocated capacity to maintain heap integrity.
Can I use HTTPS on ESP32 with a self-signed certificate?
Yes, but you must extract the self-signed certificate from your server and hardcode it into the ESP32 as the root_ca PEM string. Alternatively, if the self-signed cert regenerates frequently (like on some local Docker containers), use client.setInsecure() or implement a custom certificate verification callback using the underlying ESP-IDF esp_tls API, though the latter requires dropping down from the Arduino wrapper.
What is the difference between BearSSL and mbedTLS on ESP32?
Historically, ESP8266 used BearSSL, which is highly optimized for low-memory environments but lacks some modern cipher suites. The ESP32 Arduino core uses mbedTLS (provided by Espressif's ESP-IDF). mbedTLS is more robust, supports TLS 1.2 and 1.3, and handles larger certificates, but it demands significantly more RAM. You cannot easily swap mbedTLS for BearSSL on the ESP32 without rewriting the underlying network stack.






