Making the jump from HTTP to HTTPS on an embedded system is not simply a matter of changing the port number. On a desktop, your browser has access to a system-wide certificate store containing hundreds of trusted root CAs. The operating system and crypto libraries handle cipher suite negotiation automatically. On an embedded device with mbedTLS, you must:
- Embed the specific CA certificate(s) needed to verify your server
- Enable exactly the cryptographic primitives required by the server
- Configure TLS socket options correctly
This part walks through the complete implementation, followed by a detailed debugging guide for when things go wrong.
| Target | ESP32-S3-DevKitC |
| Zephyr version | 4.3.0 |
| Level | Intermediate |
Tutorial Series:
- WiFi Connectivity
- HTTP Client
- HTTPS with TLS (this part)
Prerequisites
This tutorial requires:
- Completed Part 2: HTTP Client with a working HTTP client
opensslcommand-line tool installed on your development machine (for certificate inspection)xxdutility (usually included withvimpackage) for converting certificates to C arrays
Familiarity with these concepts is helpful:
- TLS/SSL Protocol - the encryption layer for HTTPS
- X.509 Certificates - the standard format for TLS certificates
- Certificate Chains - how trust is established from server to root CA
- Subject Alternative Names (SAN) - how certificates match multiple hostnames
The Certificate Challenge
Before writing any code, we need to understand what certificate to embed. Let us investigate. Using openssl s_client from a development machine:
openssl s_client -connect jsonplaceholder.typicode.com:443 -showcerts
The output reveals the certificate chain the server sends. For jsonplaceholder.typicode.com, the chain looks like:
0 s:CN = typicode.com
i:C = US, O = Google Trust Services, CN = WE1
1 s:C = US, O = Google Trust Services, CN = WE1
i:C = US, O = Google Trust Services, CN = GTS Root R4
2 s:C = US, O = Google Trust Services, CN = GTS Root R4
i:C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA
This is a cross-signed chain. The server certificate is signed by WE1, which is signed by GTS Root R4. But instead of GTS Root R4 being self-signed, it is cross-signed by GlobalSign Root CA for broader compatibility with older systems.
The trust anchor we need is GlobalSign Root CA, not GTS Root R4.
Obtaining the CA Certificate
A common mistake is attempting to extract the root CA from the server’s certificate chain. However, servers typically do not send the root CA itself - they send intermediates that are signed by the root. In our case, the server sends GTS Root R4 cross-signed by GlobalSign, but not GlobalSign Root CA itself.
You must obtain the root CA from a trusted source. GlobalSign publishes their root certificates at their website:
# Download GlobalSign Root CA directly from GlobalSign
curl -o globalsign_root.crt https://secure.globalsign.com/cacert/root-r1.crt
# Verify it is the correct certificate
openssl x509 -in globalsign_root.crt -noout -subject -issuer
# Should show: subject=...CN = GlobalSign Root CA
# issuer=...CN = GlobalSign Root CA (self-signed)
# Convert to DER format
openssl x509 -in globalsign_root.crt -outform DER -out globalsign_root.der
# Generate C array
xxd -i globalsign_root.der > ca_certificate.h
The xxd -i command generates a C source file with the binary data as an array. The output looks something like:
unsigned char globalsign_root_der[] = {
0x30, 0x82, 0x03, 0x75, ...
};
unsigned int globalsign_root_der_len = 889;
Copy this file to src/ca_certificate.h and wrap it with proper header guards and defines. You will need to:
- Add
#ifndef/#define/#endifheader guards - Rename the array from
globalsign_root_dertoca_certificate - Add
static constqualifiers to the array - Add
CA_CERTIFICATE_TAG(a numeric ID for the credential) andTLS_PEER_HOSTNAMEdefines
The final src/ca_certificate.h should look like:
/*
* GlobalSign Root CA Certificate
* Valid until: 2028-01-28
* Chain: typicode.com -> WE1 -> GTS Root R4 (cross-signed) -> GlobalSign Root CA
*/
#ifndef CA_CERTIFICATE_H
#define CA_CERTIFICATE_H
#define CA_CERTIFICATE_TAG 1
#define TLS_PEER_HOSTNAME "jsonplaceholder.typicode.com"
/* GlobalSign Root CA - DER format (889 bytes) */
static const unsigned char ca_certificate[] = {
0x30, 0x82, 0x03, 0x75, 0x30, 0x82, 0x02, 0x5d, 0xa0, 0x03, 0x02, 0x01,
0x02, 0x02, 0x0b, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01, 0x15, 0x4b, 0x5a,
0xc3, 0x94, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x57, 0x31, 0x0b, 0x30, 0x09, 0x06,
0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x42, 0x45, 0x31, 0x19, 0x30, 0x17,
0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x47, 0x6c, 0x6f, 0x62, 0x61,
0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x6e, 0x76, 0x2d, 0x73, 0x61, 0x31,
0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x07, 0x52, 0x6f,
0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55,
0x04, 0x03, 0x13, 0x12, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53, 0x69,
0x67, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x30, 0x1e,
0x17, 0x0d, 0x39, 0x38, 0x30, 0x39, 0x30, 0x31, 0x31, 0x32, 0x30, 0x30,
0x30, 0x30, 0x5a, 0x17, 0x0d, 0x32, 0x38, 0x30, 0x31, 0x32, 0x38, 0x31,
0x32, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x57, 0x31, 0x0b, 0x30, 0x09,
0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x42, 0x45, 0x31, 0x19, 0x30,
0x17, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x10, 0x47, 0x6c, 0x6f, 0x62,
0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x20, 0x6e, 0x76, 0x2d, 0x73, 0x61,
0x31, 0x10, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x07, 0x52,
0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x31, 0x1b, 0x30, 0x19, 0x06, 0x03,
0x55, 0x04, 0x03, 0x13, 0x12, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x53,
0x69, 0x67, 0x6e, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x30,
0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30,
0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xda, 0x0e, 0xe6, 0x99,
0x8d, 0xce, 0xa3, 0xe3, 0x4f, 0x8a, 0x7e, 0xfb, 0xf1, 0x8b, 0x83, 0x25,
0x6b, 0xea, 0x48, 0x1f, 0xf1, 0x2a, 0xb0, 0xb9, 0x95, 0x11, 0x04, 0xbd,
0xf0, 0x63, 0xd1, 0xe2, 0x67, 0x66, 0xcf, 0x1c, 0xdd, 0xcf, 0x1b, 0x48,
0x2b, 0xee, 0x8d, 0x89, 0x8e, 0x9a, 0xaf, 0x29, 0x80, 0x65, 0xab, 0xe9,
0xc7, 0x2d, 0x12, 0xcb, 0xab, 0x1c, 0x4c, 0x70, 0x07, 0xa1, 0x3d, 0x0a,
0x30, 0xcd, 0x15, 0x8d, 0x4f, 0xf8, 0xdd, 0xd4, 0x8c, 0x50, 0x15, 0x1c,
0xef, 0x50, 0xee, 0xc4, 0x2e, 0xf7, 0xfc, 0xe9, 0x52, 0xf2, 0x91, 0x7d,
0xe0, 0x6d, 0xd5, 0x35, 0x30, 0x8e, 0x5e, 0x43, 0x73, 0xf2, 0x41, 0xe9,
0xd5, 0x6a, 0xe3, 0xb2, 0x89, 0x3a, 0x56, 0x39, 0x38, 0x6f, 0x06, 0x3c,
0x88, 0x69, 0x5b, 0x2a, 0x4d, 0xc5, 0xa7, 0x54, 0xb8, 0x6c, 0x89, 0xcc,
0x9b, 0xf9, 0x3c, 0xca, 0xe5, 0xfd, 0x89, 0xf5, 0x12, 0x3c, 0x92, 0x78,
0x96, 0xd6, 0xdc, 0x74, 0x6e, 0x93, 0x44, 0x61, 0xd1, 0x8d, 0xc7, 0x46,
0xb2, 0x75, 0x0e, 0x86, 0xe8, 0x19, 0x8a, 0xd5, 0x6d, 0x6c, 0xd5, 0x78,
0x16, 0x95, 0xa2, 0xe9, 0xc8, 0x0a, 0x38, 0xeb, 0xf2, 0x24, 0x13, 0x4f,
0x73, 0x54, 0x93, 0x13, 0x85, 0x3a, 0x1b, 0xbc, 0x1e, 0x34, 0xb5, 0x8b,
0x05, 0x8c, 0xb9, 0x77, 0x8b, 0xb1, 0xdb, 0x1f, 0x20, 0x91, 0xab, 0x09,
0x53, 0x6e, 0x90, 0xce, 0x7b, 0x37, 0x74, 0xb9, 0x70, 0x47, 0x91, 0x22,
0x51, 0x63, 0x16, 0x79, 0xae, 0xb1, 0xae, 0x41, 0x26, 0x08, 0xc8, 0x19,
0x2b, 0xd1, 0x46, 0xaa, 0x48, 0xd6, 0x64, 0x2a, 0xd7, 0x83, 0x34, 0xff,
0x2c, 0x2a, 0xc1, 0x6c, 0x19, 0x43, 0x4a, 0x07, 0x85, 0xe7, 0xd3, 0x7c,
0xf6, 0x21, 0x68, 0xef, 0xea, 0xf2, 0x52, 0x9f, 0x7f, 0x93, 0x90, 0xcf,
0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x42, 0x30, 0x40, 0x30, 0x0e, 0x06,
0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x01,
0x06, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04,
0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d,
0x0e, 0x04, 0x16, 0x04, 0x14, 0x60, 0x7b, 0x66, 0x1a, 0x45, 0x0d, 0x97,
0xca, 0x89, 0x50, 0x2f, 0x7d, 0x04, 0xcd, 0x34, 0xa8, 0xff, 0xfc, 0xfd,
0x4b, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01,
0x01, 0x05, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0xd6, 0x73, 0xe7,
0x7c, 0x4f, 0x76, 0xd0, 0x8d, 0xbf, 0xec, 0xba, 0xa2, 0xbe, 0x34, 0xc5,
0x28, 0x32, 0xb5, 0x7c, 0xfc, 0x6c, 0x9c, 0x2c, 0x2b, 0xbd, 0x09, 0x9e,
0x53, 0xbf, 0x6b, 0x5e, 0xaa, 0x11, 0x48, 0xb6, 0xe5, 0x08, 0xa3, 0xb3,
0xca, 0x3d, 0x61, 0x4d, 0xd3, 0x46, 0x09, 0xb3, 0x3e, 0xc3, 0xa0, 0xe3,
0x63, 0x55, 0x1b, 0xf2, 0xba, 0xef, 0xad, 0x39, 0xe1, 0x43, 0xb9, 0x38,
0xa3, 0xe6, 0x2f, 0x8a, 0x26, 0x3b, 0xef, 0xa0, 0x50, 0x56, 0xf9, 0xc6,
0x0a, 0xfd, 0x38, 0xcd, 0xc4, 0x0b, 0x70, 0x51, 0x94, 0x97, 0x98, 0x04,
0xdf, 0xc3, 0x5f, 0x94, 0xd5, 0x15, 0xc9, 0x14, 0x41, 0x9c, 0xc4, 0x5d,
0x75, 0x64, 0x15, 0x0d, 0xff, 0x55, 0x30, 0xec, 0x86, 0x8f, 0xff, 0x0d,
0xef, 0x2c, 0xb9, 0x63, 0x46, 0xf6, 0xaa, 0xfc, 0xdf, 0xbc, 0x69, 0xfd,
0x2e, 0x12, 0x48, 0x64, 0x9a, 0xe0, 0x95, 0xf0, 0xa6, 0xef, 0x29, 0x8f,
0x01, 0xb1, 0x15, 0xb5, 0x0c, 0x1d, 0xa5, 0xfe, 0x69, 0x2c, 0x69, 0x24,
0x78, 0x1e, 0xb3, 0xa7, 0x1c, 0x71, 0x62, 0xee, 0xca, 0xc8, 0x97, 0xac,
0x17, 0x5d, 0x8a, 0xc2, 0xf8, 0x47, 0x86, 0x6e, 0x2a, 0xc4, 0x56, 0x31,
0x95, 0xd0, 0x67, 0x89, 0x85, 0x2b, 0xf9, 0x6c, 0xa6, 0x5d, 0x46, 0x9d,
0x0c, 0xaa, 0x82, 0xe4, 0x99, 0x51, 0xdd, 0x70, 0xb7, 0xdb, 0x56, 0x3d,
0x61, 0xe4, 0x6a, 0xe1, 0x5c, 0xd6, 0xf6, 0xfe, 0x3d, 0xde, 0x41, 0xcc,
0x07, 0xae, 0x63, 0x52, 0xbf, 0x53, 0x53, 0xf4, 0x2b, 0xe9, 0xc7, 0xfd,
0xb6, 0xf7, 0x82, 0x5f, 0x85, 0xd2, 0x41, 0x18, 0xdb, 0x81, 0xb3, 0x04,
0x1c, 0xc5, 0x1f, 0xa4, 0x80, 0x6f, 0x15, 0x20, 0xc9, 0xde, 0x0c, 0x88,
0x0a, 0x1d, 0xd6, 0x66, 0x55, 0xe2, 0xfc, 0x48, 0xc9, 0x29, 0x26, 0x69,
0xe0
};
#endif /* CA_CERTIFICATE_H */
Creating the TLS Overlay
Zephyr supports configuration overlays that add features without modifying the base prj.conf. Create overlay-tls.conf:
# TLS/HTTPS overlay configuration for ESP32-S3
# Build with: west build -b esp32s3_devkitc/esp32s3/procpu -- -DEXTRA_CONF_FILE=overlay-tls.conf
# Increased stack for TLS handshake
CONFIG_MAIN_STACK_SIZE=16384
# Increased network buffers for TLS
CONFIG_NET_BUF_RX_COUNT=80
CONFIG_NET_BUF_TX_COUNT=80
# mbedTLS core
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=60000
# Server certificate chain requires larger buffer
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
# SHA algorithms for certificate verification
# SHA-1 for GlobalSign Root CA signature verification
CONFIG_MBEDTLS_SHA1=y
# SHA-256 for intermediate certificates and TLS handshake
CONFIG_MBEDTLS_SHA256=y
# SHA-384/512 for GTS Root R4 (ECDSA P-384)
CONFIG_MBEDTLS_SHA384=y
CONFIG_MBEDTLS_SHA512=y
# RSA for GlobalSign Root CA verification (RSA-2048)
CONFIG_MBEDTLS_RSA_C=y
CONFIG_MBEDTLS_PKCS1_V15=y
# ECDH and ECDSA for key exchange (server uses ECDHE-ECDSA)
CONFIG_MBEDTLS_ECDH_C=y
CONFIG_MBEDTLS_ECDSA_C=y
CONFIG_MBEDTLS_ECP_C=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y
# AES-GCM cipher (server supports ECDHE-ECDSA-AES128-GCM-SHA256)
CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y
CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y
# Key exchange modes
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED=y
# Server Name Indication - REQUIRED for hostname verification against SAN wildcards
CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y
# TLS socket support
CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
CONFIG_NET_SOCKETS_TLS_MAX_CONTEXTS=2
# Credential storage
CONFIG_TLS_CREDENTIALS=y
CONFIG_TLS_MAX_CREDENTIALS_NUMBER=2
That is a lot of options. The reasoning behind each one is covered in the Appendix: Debugging TLS section at the end of this tutorial.
HTTPS Client Code
The code changes from HTTP to HTTPS are surprisingly minimal. Update src/main.c:
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/wifi_mgmt.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_event.h>
#include <zephyr/net/socket.h>
#include <zephyr/net/http/client.h>
#include <zephyr/net/tls_credentials.h>
#include <zephyr/posix/netdb.h>
#include <zephyr/posix/unistd.h>
#include <zephyr/posix/sys/socket.h>
#include <errno.h>
#include "ca_certificate.h"
LOG_MODULE_REGISTER(wifi_app, LOG_LEVEL_INF);
#define HTTPS_HOST "jsonplaceholder.typicode.com"
#define HTTPS_PORT "443"
#define HTTPS_PATH "/todos/1"
static struct net_mgmt_event_callback wifi_cb;
static struct net_mgmt_event_callback ipv4_cb;
static K_SEM_DEFINE(ip_obtained_sem, 0, 1);
static uint8_t recv_buf[512];
static int response_cb(struct http_response *rsp,
enum http_final_call final_data,
void *user_data)
{
if (final_data == HTTP_DATA_MORE) {
LOG_INF("Partial data received (%zd bytes)", rsp->data_len);
} else if (final_data == HTTP_DATA_FINAL) {
LOG_INF("HTTP Status: %s", rsp->http_status);
LOG_INF("Response body (%zd bytes):", rsp->data_len);
LOG_INF("%.*s", (int)rsp->data_len, rsp->recv_buf);
}
return 0;
}
static int https_get_request(void)
{
struct addrinfo hints;
struct addrinfo *res;
int sock;
int ret;
LOG_INF("Resolving %s...", HTTPS_HOST);
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
ret = getaddrinfo(HTTPS_HOST, HTTPS_PORT, &hints, &res);
if (ret != 0) {
LOG_ERR("DNS lookup failed: %d", ret);
return ret;
}
LOG_INF("DNS resolved, creating TLS socket...");
/* Key change: IPPROTO_TLS_1_2 instead of IPPROTO_TCP */
sock = socket(res->ai_family, res->ai_socktype, IPPROTO_TLS_1_2);
if (sock < 0) {
LOG_ERR("TLS socket creation failed: %d", errno);
freeaddrinfo(res);
return -errno;
}
/* Configure TLS credentials */
sec_tag_t sec_tag_list[] = { CA_CERTIFICATE_TAG };
ret = setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
sec_tag_list, sizeof(sec_tag_list));
if (ret < 0) {
LOG_ERR("Failed to set TLS_SEC_TAG_LIST: %d", errno);
close(sock);
freeaddrinfo(res);
return -errno;
}
/* Set hostname for certificate verification */
ret = setsockopt(sock, SOL_TLS, TLS_HOSTNAME,
TLS_PEER_HOSTNAME, sizeof(TLS_PEER_HOSTNAME));
if (ret < 0) {
LOG_ERR("Failed to set TLS_HOSTNAME: %d", errno);
close(sock);
freeaddrinfo(res);
return -errno;
}
LOG_INF("Connecting to %s:%s (TLS)...", HTTPS_HOST, HTTPS_PORT);
/* connect() triggers the TLS handshake */
ret = connect(sock, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
if (ret < 0) {
LOG_ERR("TLS connect failed: %d", errno);
close(sock);
return -errno;
}
LOG_INF("TLS handshake complete, sending HTTPS GET %s...", HTTPS_PATH);
/* From here, everything is identical to plain HTTP */
struct http_request req = {
.method = HTTP_GET,
.url = HTTPS_PATH,
.host = HTTPS_HOST,
.protocol = "HTTP/1.1",
.response = response_cb,
.recv_buf = recv_buf,
.recv_buf_len = sizeof(recv_buf),
};
ret = http_client_req(sock, &req, 10 * MSEC_PER_SEC, NULL);
if (ret < 0) {
LOG_ERR("HTTPS request failed: %d", ret);
} else {
LOG_INF("HTTPS request completed (%d bytes sent)", ret);
}
close(sock);
return ret;
}
static void wifi_event_handler(struct net_mgmt_event_callback *cb,
uint64_t mgmt_event, struct net_if *iface)
{
if (mgmt_event == NET_EVENT_WIFI_CONNECT_RESULT) {
LOG_INF("WiFi connected");
} else if (mgmt_event == NET_EVENT_WIFI_DISCONNECT_RESULT) {
LOG_INF("WiFi disconnected");
}
}
static void ipv4_event_handler(struct net_mgmt_event_callback *cb,
uint64_t mgmt_event, struct net_if *iface)
{
if (mgmt_event == NET_EVENT_IPV4_ADDR_ADD) {
struct net_if_ipv4 *ipv4 = iface->config.ip.ipv4;
if (ipv4) {
char addr_str[NET_IPV4_ADDR_LEN];
for (int i = 0; i < NET_IF_MAX_IPV4_ADDR; i++) {
if (ipv4->unicast[i].ipv4.is_used) {
net_addr_ntop(AF_INET,
&ipv4->unicast[i].ipv4.address.in_addr,
addr_str, sizeof(addr_str));
LOG_INF("IP Address: %s", addr_str);
}
}
}
k_sem_give(&ip_obtained_sem);
}
}
static int connect_wifi(void)
{
struct net_if *iface = net_if_get_default();
struct wifi_connect_req_params params = {
.ssid = CONFIG_WIFI_SSID,
.ssid_length = strlen(CONFIG_WIFI_SSID),
.psk = CONFIG_WIFI_PSK,
.psk_length = strlen(CONFIG_WIFI_PSK),
.channel = WIFI_CHANNEL_ANY,
.band = WIFI_FREQ_BAND_2_4_GHZ,
.security = WIFI_SECURITY_TYPE_PSK,
};
LOG_INF("Connecting to %s...", CONFIG_WIFI_SSID);
return net_mgmt(NET_REQUEST_WIFI_CONNECT, iface, ¶ms, sizeof(params));
}
int main(void)
{
int ret;
LOG_INF("ESP32-S3 WiFi + HTTPS Client Example");
/* Register CA certificate for TLS */
ret = tls_credential_add(CA_CERTIFICATE_TAG,
TLS_CREDENTIAL_CA_CERTIFICATE,
ca_certificate, sizeof(ca_certificate));
if (ret < 0) {
LOG_ERR("Failed to register CA certificate: %d", ret);
return ret;
}
/* Register WiFi event callback */
net_mgmt_init_event_callback(&wifi_cb, wifi_event_handler,
NET_EVENT_WIFI_CONNECT_RESULT |
NET_EVENT_WIFI_DISCONNECT_RESULT);
net_mgmt_add_event_callback(&wifi_cb);
/* Register IPv4 event callback */
net_mgmt_init_event_callback(&ipv4_cb, ipv4_event_handler,
NET_EVENT_IPV4_ADDR_ADD);
net_mgmt_add_event_callback(&ipv4_cb);
/* Wait for WiFi to initialize */
k_sleep(K_SECONDS(2));
/* Connect to WiFi */
ret = connect_wifi();
if (ret) {
LOG_ERR("WiFi connect request failed: %d", ret);
return ret;
}
/* Wait for IP address */
LOG_INF("Waiting for IP address...");
ret = k_sem_take(&ip_obtained_sem, K_SECONDS(30));
if (ret < 0) {
LOG_ERR("Timeout waiting for IP address");
return ret;
}
/* Small delay to ensure network stack is ready */
k_sleep(K_MSEC(500));
/* Make HTTPS request */
ret = https_get_request();
if (ret < 0) {
LOG_ERR("HTTPS GET failed: %d", ret);
}
return 0;
}
Key Differences from HTTP
- Protocol:
IPPROTO_TLS_1_2instead ofIPPROTO_TCP - Credential registration:
tls_credential_add()at startup links the CA certificate to a security tag - Socket options:
setsockopt()withSOL_TLSconfigures the security tag list and hostname - Transparent handshake: The
connect()call performs the TLS handshake automatically
Once connected, the HTTP layer is identical - http_client_req() works the same way over the encrypted channel.
Building and Flashing
Note the overlay file specification:
west build -b esp32s3_devkitc/esp32s3/procpu -- -DEXTRA_CONF_FILE=overlay-tls.conf
west flash
west espressif monitor
Expected Output
[00:00:00.000,000] <inf> wifi_app: ESP32-S3 WiFi + HTTPS Client Example
[00:00:02.000,000] <inf> wifi_app: Connecting to YourSSID...
[00:00:02.000,000] <inf> wifi_app: Waiting for IP address...
[00:00:05.xxx,xxx] <inf> wifi_app: WiFi connected
[00:00:06.xxx,xxx] <inf> wifi_app: IP Address: 192.168.x.x
[00:00:06.xxx,xxx] <inf> wifi_app: Resolving jsonplaceholder.typicode.com...
[00:00:06.xxx,xxx] <inf> wifi_app: DNS resolved, creating TLS socket...
[00:00:06.xxx,xxx] <inf> wifi_app: Connecting to jsonplaceholder.typicode.com:443 (TLS)...
[00:00:08.xxx,xxx] <inf> wifi_app: TLS handshake complete, sending HTTPS GET /todos/1...
[00:00:08.xxx,xxx] <inf> wifi_app: HTTP Status: 200 OK
[00:00:08.xxx,xxx] <inf> wifi_app: Response body (83 bytes):
[00:00:08.xxx,xxx] <inf> wifi_app: {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
[00:00:08.xxx,xxx] <inf> wifi_app: HTTPS request completed (91 bytes sent)
Conclusion
We have built a complete HTTPS client on the ESP32-S3, progressing from plain HTTP to fully verified TLS connections. The key points to remember:
- Certificate chains can be complex; cross-signing means the obvious root CA may not be the right one
- mbedTLS requires explicit configuration of every cryptographic primitive
- SNI is essential not just for virtual hosting, but for wildcard certificate verification
- Zephyr’s TLS socket API abstracts the handshake behind standard
connect()
The embedded GlobalSign Root CA certificate is valid until 2028, but certificate chains do change. If you see verification failures in the future, revisit the certificate chain with openssl s_client -showcerts.
So what will you secure next?
Appendix: Debugging TLS - A Journey Through Error Codes
This section documents the iterative debugging process that led to the final working configuration. If your TLS connection fails, these errors and their solutions may help.
Error 1: -0x262e (X509_INVALID_FORMAT)
The certificate fails to parse. The GlobalSign Root CA uses RSA-2048, but mbedTLS RSA support is not enabled by default.
Fix: Add RSA support:
CONFIG_MBEDTLS_RSA_C=y
CONFIG_MBEDTLS_PKCS1_V15=y
Error 2: -0x7100 (SSL_BAD_CONFIG)
“No ciphersuite in common” - the server and client cannot agree on encryption algorithms. Checking the server:
openssl s_client -connect jsonplaceholder.typicode.com:443 2>/dev/null | grep "Cipher"
Output shows ECDHE-based cipher suites.
Fix: Enable ECDH, ECDSA, and AES-GCM:
CONFIG_MBEDTLS_ECDH_C=y
CONFIG_MBEDTLS_ECDSA_C=y
CONFIG_MBEDTLS_ECP_C=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y
CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y
CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED=y
Error 3: “requesting more data than fits”
The SSL buffer cannot hold the full certificate chain. The server sends multiple certificates during the handshake.
Fix: Increase the SSL buffer:
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
Error 4: -0x2700 with verification flags 0x0c
Now we reach certificate verification. The error code -0x2700 is MBEDTLS_ERR_X509_CERT_VERIFY_FAILED. The flags tell us why:
0x04=MBEDTLS_X509_BADCERT_CN_MISMATCH- hostname mismatch0x08=MBEDTLS_X509_BADCERT_NOT_TRUSTED- certificate not signed by trusted CA
Flag 0x0c is both (0x04 | 0x08). This occurred when using the wrong root CA (GTS Root R1 instead of GlobalSign). The server sends a cross-signed chain, and we need the ultimate trust anchor.
Fix: Switch to GlobalSign Root CA as the embedded certificate.
Error 5: -0x2700 with verification flags 0x04 (CN_MISMATCH)
The certificate is now trusted, but hostname verification still fails. The server certificate has:
- Subject CN:
typicode.com - Subject Alternative Names:
DNS:typicode.com, DNS:*.typicode.com
We are connecting to jsonplaceholder.typicode.com, which should match the wildcard *.typicode.com. Why does it fail?
The Critical Missing Piece: Server Name Indication (SNI)
SNI serves two purposes:
- Tells the server which hostname we are requesting (for virtual hosting)
- Enables mbedTLS to perform proper hostname verification against SAN wildcards
Without SNI enabled, mbedTLS cannot match the hostname against wildcard entries in the certificate’s Subject Alternative Names.
Fix:
CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y
With this addition, the handshake succeeds.
Troubleshooting Quick Reference
When TLS fails, mbedTLS reports negative hex error codes. Here is a quick reference:
| Error Code | Meaning | Likely Cause |
|---|---|---|
-0x262e |
X509_INVALID_FORMAT | Certificate parsing failed. Enable RSA (CONFIG_MBEDTLS_RSA_C) |
-0x7100 |
SSL_BAD_CONFIG | No common ciphersuite. Check server’s supported algorithms |
-0x2700 |
CERT_VERIFY_FAILED | Decode the verification flags (see below) |
-0x6c00 |
SSL_BUFFER_TOO_SMALL | Increase CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN |
Verification flags (from -0x2700 errors):
| Flag | Meaning |
|---|---|
0x01 |
Certificate expired |
0x02 |
Certificate revoked |
0x04 |
CN/SAN mismatch (hostname verification failed) |
0x08 |
Not signed by trusted CA |
0x10 |
CRL not available |
Flags combine: 0x0c means both CN_MISMATCH (0x04) and NOT_TRUSTED (0x08).
Further Reading
Zephyr Documentation:
- Zephyr TLS Credentials API - credential management functions
- Zephyr Secure Sockets - TLS socket options and usage
- Zephyr TLS Sample - official echo client with TLS support
mbedTLS Documentation:
- mbedTLS Documentation - comprehensive library documentation
- mbedTLS Error Codes - complete error code reference
- mbedTLS Configuration - understanding build-time configuration
TLS and Certificates:
- SSL Labs Server Test - analyze any server’s TLS configuration
- OpenSSL Cookbook - free resource for certificate handling
- Let’s Encrypt Documentation - understanding modern CA infrastructure
- Mozilla SSL Configuration Generator - recommended cipher suite configurations
Certificate Debugging Tools:
openssl s_client -connect host:443 -showcerts- view certificate chainopenssl x509 -in cert.pem -text -noout- decode certificate detailsopenssl verify -CAfile root.pem chain.pem- verify certificate chain locally