https_request 例程是一个HTTPS请求演示,展示了怎么使用ESP-TLS库来建立HTTPS的通信过程,请发送HTTP GET请求。
其中ESP-TLS 库是 ESP-IDF 对底层 TLS (mbedTLS 或者wolfSSL) 做的一层封装,提供了更高层、更简洁的接口,方便快速发起 HTTPS 请求,屏蔽了较多底层细节。用户可以设置选择底层的TLS库,默认情况下,使用mbedTLS 库。

一、ESP-TLS 简介

ESP-TLS 组件提供简化 API 接口,用于访问常用 TLS 功能,支持如 CA 认证验证、SNI、ALPN 协商和非阻塞连接等常见场景,相关配置可在数据结构体 esp_tls_cfg_t 中指定。配置完成后,使用以下 API 进行 TLS 通信:

esp_tls_init():初始化 TLS 连接句柄。
esp_tls_conn_new_sync():开启新的阻塞式 TLS 连接。
esp_tls_conn_new_async():开启新的非阻塞式 TLS 连接。
esp_tls_conn_read():读取 TLS 层之上的应用数据。
esp_tls_conn_write():将应用数据写入 TLS 连接。
esp_tls_conn_destroy():释放连接。

任何应用层协议,如 HTTP1、HTTP2 等,均可调用 ESP-TLS 组件接口实现。

1. ESP-TLS 组件的树形结构

├── esp_tls.c
├── esp_tls.h
├── esp_tls_mbedtls.c
├── esp_tls_wolfssl.c
└── private_include
    ├── esp_tls_mbedtls.h
    └── esp_tls_wolfssl.h

ESP-TLS 组件文件 esp-tls/esp_tls.h 包含该组件的公共 API 头文件。在 ESP-TLS 组件内部,为了实现安全会话功能,会使用 MbedTLS 和 WolfSSL 两个 SSL/TLS 库中的其中一个进行安全会话的建立,与 MbedTLS 相关的 API 存放在 esp-tls/private_include/esp_tls_mbedtls.h,而与 WolfSSL 相关的 API 存放在 esp-tls/private_include/esp_tls_wolfssl.h

2. TLS 服务器验证

ESP-TLS 在客户端提供了多种验证 TLS 服务器的选项,如验证对端服务器的服务器证书、或使用预共享密钥验证服务器。用户应在 esp_tls_cfg_t 结构体中选择以下任一选项完成 TLS 服务器验证,若未做选择,则客户端默认在 TLS 连接创建时,会返回错误。

TLS中有以下几种验证方式:

  • cacert_bufcacert_bytes:以缓冲区的形式向 esp_tls_cfg_t 结构体提供 CA 证书,ESP-TLS 将使用缓冲区中的 CA 证书验证服务器。注意,须在 esp_tls_cfg_t 结构体中设置以下变量:

    • cacert_buf - 指针,指向包含 CA 证书的缓冲区。
    • cacert_bytes - CA 证书大小(以字节为单位)。
  • use_global_ca_storeglobal_ca_store 把CA证书存放在全局存储区,只需要初始化一次,后续所有的TLS连接,只需要在的 esp_tls_cfg_t 结构体中设置 use_global_ca_store = true,就能使用该CA验证 ESP-TLS 连接的服务器。

  • crt_bundle_attach:ESP x509 证书包 API 提供了便捷的服务器验证方法,即打包一组自定义的 x509 根证书,用于 TLS 服务器验证,详情请参阅 ESP x509 证书包

  • psk_hint_key:要使用预共享密钥验证服务器,必须在 ESP-TLS menuconfig 中启用 CONFIG_ESP_TLS_PSK_VERIFICATION,然后向结构体 esp_tls_cfg_t 提供指向 PSK 提示和密钥的指针。若未选择有关服务器验证的其他选项,ESP-TLS 将仅用 PSK 验证服务器。

  • 跳过服务器验证:该选项并不安全,仅供测试使用。在 ESP-TLS menuconfig 中启用 CONFIG_ESP_TLS_INSECURECONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY 可启用该选项,此时,若未在 esp_tls_cfg_t 结构体选择其他服务器验证选项,ESP-TLS 将默认跳过服务器验证。

2.1 全局 CA Store 和 普通方式(缓冲区配置 CA)的区别


1. 缓冲区配置(每次手动配置 CA)

做法:在每一次 TLS 连接时,把证书传给 esp_tls_cfg_t

Cesp_tls_cfg_t cfg = {
    .cacert_pem_buf   = server_root_cert_pem_start,
    .cacert_pem_bytes = server_root_cert_pem_end - server_root_cert_pem_start,
};

📌 特点:

  • 每个连接都会单独从 PEM 解析证书(mbedtls_x509_crt_parse())。
  • 如果你在多个地方频繁创建 HTTPS 连接,会有 重复解析 + 内存开销
  • 灵活,可以针对不同服务器传不同的根 CA。

2. 全局 CA Store

做法:在程序启动时,先加载一次全局根证书:

Cesp_tls_set_global_ca_store(server_root_cert_pem_start,
                            server_root_cert_pem_end - server_root_cert_pem_start);

esp_tls_cfg_t cfg = {
    .use_global_ca_store = true,
};

📌 特点:

  • 证书只在第一次解析并存进 RAM,后续所有连接复用。
  • 减少多次解析同一个证书的开销(CPU 和堆内存)。
  • 适合你只使用一套根 CA 的情况。

3. 核心区别对比

对比点 普通方式:手动配置 CA 全局 CA Store
证书加载 每次连接时都加载/解析 PEM 程序启动时加载一次
内存占用 每个连接可有独立副本 所有连接共享一份证书链
CPU 开销 每次连接时都要 PEM → X.509 解析 只解析一次,后续直接复用
灵活性 可以为每个服务器配置不同 CA 所有连接共用同一份全局 CA
适用场景 多服务、多根证书环境 单一或少量固定根 CA,频繁新建连接

2.2 使用ESP x509 证书包

在mbedtls 配置中开启 CONFIG_MBEDTLS_CERTIFICATE_BUNDLE = 1 时,SDK 会把一份常见的 公共 CA 证书集合(根证书 bundle)编译到固件里。

  • 当你调用 https_get_request_using_crt_bundle() 时,实际上是使用 esp_crt_bundle_attach 自动加载那份全局内置根证书集合。

3. SNI(服务器名称指示)

SNI 是 TLS 协议的一个扩展,它能让客户端在 TLS 握手过程中,主动指定要连接的主机名。当服务器通过同一个 IP 地址托管多个域名时,客户端必须使用这一功能才能成功建立连接。

如何确保 SNI 正常工作:

  • 使用 HTTPS 连接时,ESP-TLS 默认启用 SNI,无需额外配置。
  • 若需手动指定 SNI 主机名,请使用 esp_tls_cfg_t 中的 common_name 字段进行设置,确保在握手过程中向服务器发送正确的主机名。
  • common_name 的值必须与服务器证书中的通用名称 (CN, Common Name) 完全匹配。
  • 需将 skip_common_name 字段设置为 false,确保服务器证书能通过主机名完成正确验证。这是 SNI 正常运行的必要条件。

4. ESP-TLS 服务器证书选择回调

使用 MbedTLS 协议栈时,ESP-TLS 组件支持设置服务器证书选择回调函数。此时,在服务器握手期间可选择使用哪个服务器证书,该回调可获取客户端发送的 “Client Hello” 消息中提供的 TLS 扩展(ALPN、SPI 等),并基于此选择传输哪个服务器证书给客户端。要启用此功能,请在 ESP-TLS menuconfig 中启用 CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK

证书选择回调可在结构体 esp_tls_cfg_t 中配置,具体如下:

int cert_selection_callback(mbedtls_ssl_context *ssl)
{
    /* 回调应执行的代码 */
    return 0;
}

esp_tls_cfg_t cfg = {
    cert_select_cb = cert_section_callback,
};

5. 客户端会话票据

ESP-TLS 支持客户端会话恢复,可以在后续与同一服务器连接时避免完整的 TLS 握手,节省时间和资源。该功能在 ESP-TLS 使用 MbedTLS 作为底层协议栈时可用。

会话恢复机制在不同 TLS 版本中略有差异:

  • TLS 1.2:通过会话 ID(由 TLS 协议栈内部管理)或会话票据(参见 RFC 5077)实现会话恢复。ESP-TLS 主要采用会话票据机制,从而显式控制应用程序。
  • TLS 1.3:会话恢复完全依赖会话票据实现。服务器会在主握手完成后,通过 “NewSessionTicket” 消息发送票据。与 TLS 1.2 不同,这些票据可以在会话期间的任意时刻发送,无需在握手后立即完成。

要启用和使用客户端会话票据的步骤如下:

  1. 启用 Kconfig 选项 CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS

  2. 在成功建立 TLS 连接(并完成握手)后,使用 esp_tls_get_client_session() 获取会话票据。

    • 对于 TLS 1.3:会话票据可能在握手后由服务器随时发送,因此应用程序应定期或在特定的应用层交互之后调用 esp_tls_get_client_session(),确保获取最新的票据。TLS 协议栈接收并处理的每个新票据都会覆盖之前的票据,用于后续的会话恢复。
  3. 将此会话票据安全地存储起来。

  4. 后续连接同一服务器时,将存储的会话票据填入 esp_tls_cfg_t::client_session 字段。

  5. 在不再需要客户端会话或获取新会话前,应调用 esp_tls_free_client_session() 释放客户端会话上下文。

#include "esp_tls.h"

// 全局或持久存储客户端会话
esp_tls_client_session_t *saved_session = NULL;

void connect_to_server(bool use_saved_session_arg) {
    esp_tls_cfg_t cfg = {0}; // 按需初始化配置参数
    // ... 设置其他 cfg 成员,如 cacert_buf, common_name 等

    if (use_saved_session_arg && saved_session) {
        cfg.client_session = saved_session;
        // ESP_LOGI(TAG, "Attempting connection with saved session ticket.");
    } else {
        // ESP_LOGI(TAG, "Attempting connection without a saved session ticket (full handshake).");
    }

    esp_tls_t *tls = esp_tls_init();
    if (!tls) {
        // ESP_LOGE(TAG, "Failed to initialize ESP-TLS handle.");
        return;
    }

    if (esp_tls_conn_http_new_sync("https://your-server.com", &cfg, tls) == 1) {
        // ESP_LOGI(TAG, "Connection successful.");

        // 每次连接成功后,都要尝试获取/更新最新的会话票据。
        // 无论本次是新握手还是通过会话恢复连接,更新票据都是有益的。
        // 特别是对 TLS 1.3 而言,服务器可能会在握手完成后下发新票据。
        if (saved_session) {
            esp_tls_free_client_session(saved_session); // 释放之前的票据
            saved_session = NULL;
        }
        saved_session = esp_tls_get_client_session(tls);
        if (saved_session) {
            // ESP_LOGI(TAG, "Successfully retrieved/updated client session ticket.");
        } else {
            // ESP_LOGW(TAG, "Failed to get client session ticket even after a successful connection.");
        }

        // 执行 TLS 通信...

    }
    esp_tls_conn_destroy(tls);
}

备注

  • 从服务器获取的会话票据通常有有效期,期限由服务器决定。
  • 当尝试使用存储的票据进行连接时,如果服务器判定该票据无效(例如过期或被拒绝),ESP-TLS 会自动尝试执行完整的 TLS 握手来建立连接。在这种情况下,应用程序无需实现额外的重试逻辑。只有当会话恢复和后续的完整握手均失败时,才会报告连接失败。
  • 当不再需要 esp_tls_client_session_t 上下文时,或在获取并存储新的会话票据前,应调用 esp_tls_free_client_session() 释放会话。
  • 对于 TLS 1.3,服务器在一次连接中可能多次发送新会话票据 NewSessionTicket。每次成功调用 esp_tls_get_client_session() 都会返回由底层 TLS 协议栈处理的最新票据上下文。如果应用程序要使用最新的票据进行会话恢复,则需由应用程序负责管理并更新其存储的会话信息。

6. TLS 加密套件

ESP-TLS 支持在客户端模式下设置加密套件列表,TLS 密码套件列表用于向服务器传递所支持的密码套件信息,用户可以根据自己需求增减加密套件,且适用于任何 TLS 协议栈配置。如果服务器支持列表中的任一密码套件,则 TLS 连接成功,反之连接失败。

连接客户端时,在 esp_tls_cfg_t 结构体中设置 ciphersuites_list 的步骤如下:

/* 加密套件列表必须以 0 结尾,并且在整个 TLS 连接期间,加密套件的内存地址空间有效 */
static const int ciphersuites_list[] = {MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, MBEDTLS_TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 0};
esp_tls_cfg_t cfg = {
    .ciphersuites_list = ciphersuites_list,
};

ESP-TLS 不会检查 ciphersuites_list 的有效性,因此需调用 esp_tls_get_ciphersuites_list() 获取 TLS 协议栈中支持的加密套件列表,并检查设置的加密套件是否在支持的加密套件列表中。

备注:

  • 此功能仅在 MbedTLS 协议栈中有效。

7. TLS 协议版本

ESP-TLS 能够为 TLS 连接设置相应的 TLS 协议版本,指定版本将用于建立专用 TLS 连接。也就是说,在运行时不同的 TLS 连接可以配置到 TLS 1.2、TLS 1.3 等不同协议版本。

备注

  • 目前,仅在 MbedTLS 作为 ESP-TLS 的底层 SSL/TLS 协议栈时支持此功能。

要在 ESP-TLS 中设置 TLS 协议版本,请设置 esp_tls_cfg_t::tls_version,从 esp_tls_proto_ver_t 中选择所需版本。如未指定协议版本字段,将默认根据服务器要求建立 TLS 连接。

ESP-TLS 连接的协议版本可按如下方式配置:

#include "esp_tls.h"
esp_tls_cfg_t cfg = {
    .tls_version = ESP_TLS_VER_TLS_1_2,
};

二、HTTPS_REQUEST例程

在TLS握手阶段,客户端( ESP32)会验证服务器证书,主要包括:

  • 有效期:检查 Not BeforeNot After 字段
    • 当前时间必须在这个时间区间内,否则认为“尚未生效”或“已过期”。
  • 签名:用根 CA 公钥验证服务器证书签名。
  • 域名:证书中的 CN / SAN 必须与访问的主机名匹配。

这三步任何一步失败,TLS 都会拒绝连接。

1. 时间同步

在进行TLS连接时,需要同步系统时间,否则在证书验证阶段会验证失败。

通过如下指令可以查看当前服务器的CA根证书的有效期:

virtual-machine:~/https_request/main$ openssl x509 -in server_root_cert.pem -noout -dates 
notBefore=Jun  4 11:04:38 2015 GMT 
notAfter=Jun  4 11:04:38 2035 GMT

ESP32的系统时间不能超过这个证书的有效期,如果没有同步时间,则会报错:

“The certificate validity starts in the future”

导致证书验证失败。

因此,在TLS连接之前,需要先通过SNTP/NTP 同步系统时间。

⚠️ 取消证书到期检查

如果你不想同步时间,可以在配置中跳过证书有效期检查,之后TLS的握手时就不会检查证书有效性,后续的流程能够正常进行。

在ESP32的配置中,取消 MBEDTLS_HAVE_TIME_DATE 到期检查。

2. 证书

在HTTPS_REQUEST例程中,使用到server_root_cert.pem 根证书,该证书用来验证 www.howsmyssl.com 服务器的身份。

把该根CA证书和服务器证书,以及服务器的根CA证书进行对比:

可以看到, www.howsmyssl.com 服务器证书 由 中间证书颁发机构 Let’s Encrypt 签发,而中间证书R10 又由根证书颁发机构签发。最后侧的图示中,展示了完整的证书签发链。在本例程中的证书就是根CA证书,作为证书信任锚点,来验证整个证书链,确保ESP32 所连接的服务器身份正确。

3. 例程中的内容

1. 服务器验证设置

在本例程中,演示了多个配置情况:

#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE && CONFIG_EXAMPLE_USING_ESP_TLS_MBEDTLS
    https_get_request_using_crt_bundle();  // 使用证书包,不需要配置根证书。
#endif
    ESP_LOGI(TAG, "Minimum free heap size: %" PRIu32 " bytes", esp_get_minimum_free_heap_size());
    printf("mbedtls time: %lld\n", mbedtls_time(NULL));
    https_get_request_using_cacert_buf();  // 缓冲区配置根证书
    // https_get_request_using_global_ca_store();	// 把CA证书存放在全局存储区
    // https_get_request_using_specified_ciphersuites();  // 配置加密套件

2.客户端使用会话Ticket的示例

#ifdef CONFIG_EXAMPLE_CLIENT_SESSION_TICKETS
static void https_get_request_to_local_server(const char* url)
{
    ESP_LOGI(TAG, "https_request to local server");
    esp_tls_cfg_t cfg = {
        .cacert_buf = (const unsigned char *) local_server_cert_pem_start,
        .cacert_bytes = local_server_cert_pem_end - local_server_cert_pem_start,
        .skip_common_name = true,
    };
    save_client_session = true;
    https_get_request(cfg, url, LOCAL_SRV_REQUEST);
}

static void https_get_request_using_already_saved_session(const char *url)
{
    ESP_LOGI(TAG, "https_request using saved client session");
    esp_tls_cfg_t cfg = {
        .client_session = tls_client_session,
    };
    https_get_request(cfg, url, LOCAL_SRV_REQUEST);
    esp_tls_free_client_session(tls_client_session);
    save_client_session = false;
    tls_client_session = NULL;
}
#endif

三、文档来源

1. ESP-TLS - ESP32 - — ESP-IDF 编程指南 latest 文档

2. 数字签名与证书

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐