Connectivity with Zephyr - Part 3: HTTPS with TLS on ESP32-S3

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:

  1. Embed the specific CA certificate(s) needed to verify your server
  2. Enable exactly the cryptographic primitives required by the server
  3. 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:

  1. WiFi Connectivity
  2. HTTP Client
  3. HTTPS with TLS (this part)

Prerequisites

This tutorial requires:

  • Completed Part 2: HTTP Client with a working HTTP client
  • openssl command-line tool installed on your development machine (for certificate inspection)
  • xxd utility (usually included with vim package) for converting certificates to C arrays

Familiarity with these concepts is helpful:

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:

  1. Add #ifndef/#define/#endif header guards
  2. Rename the array from globalsign_root_der to ca_certificate
  3. Add static const qualifiers to the array
  4. Add CA_CERTIFICATE_TAG (a numeric ID for the credential) and TLS_PEER_HOSTNAME defines

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, &params, 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

  1. Protocol: IPPROTO_TLS_1_2 instead of IPPROTO_TCP
  2. Credential registration: tls_credential_add() at startup links the CA certificate to a security tag
  3. Socket options: setsockopt() with SOL_TLS configures the security tag list and hostname
  4. 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 mismatch
  • 0x08 = 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:

  1. Tells the server which hostname we are requesting (for virtual hosting)
  2. 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:

mbedTLS Documentation:

TLS and Certificates:

Certificate Debugging Tools:

  • openssl s_client -connect host:443 -showcerts - view certificate chain
  • openssl x509 -in cert.pem -text -noout - decode certificate details
  • openssl verify -CAfile root.pem chain.pem - verify certificate chain locally