☰
mbedtls源码包深度解析:嵌入式设备跑HTTPS的轻量TLS实践
2026/10/20 5:05:09 网站建设 项目流程

简介:mbedtls是开源轻量级TLS/SSL协议库,广泛用于嵌入式设备、物联网及移动应用。这份源码包共1332个文件,压缩后约13.47MB,其中202个C源文件与90个头文件构成核心代码,181个crt、142个pem、67个der等证书密钥样本用于加密测试与身份验证场景,测试套件和各类示例程序则可辅助理解API调用与协议流程。包内覆盖SSL 3.0至TLS 1.3协议栈,包含AES、3DES、RSA、ECC等对称及非对称加密算法,SHA系列哈希函数、X.509证书解析、随机数生成器等模块,并提供client/server、加密解密、证书签发等多种样例,便于对照源码学习配置与裁剪。目前已有361人下载学习,适合意在深入掌握mbedtls内部结构、学习安全通信实现,并在资源受限平台定制轻量级加密方案的嵌入式或后端开发者。

1. 嵌入式设备跑HTTPS:mbedtls源码包为什么值得扒一遍

某开发者接手一个物联网网关项目,硬件是主频 120MHz 的 MCU,内存只有 256KB,却要对接云端 REST API。第一版用了 OpenSSL,编译产物 800 多 KB,固件直接超容。换用 mbedtls 之后,整个 TLS 栈压缩到 60KB 左右,握手延迟反而还低了一些。这个经历让我对 mbedtls 源码包产生了兴趣——它怎么做到这么小,又是怎么把 TLS/HTTPS 跑起来的?这份源码包适合两类人:一是嵌入式开发者需要在资源受限设备上跑 HTTPS;二是想在 PC 端验证 TLS 实现原理、想读懂握手状态机的人。拆完源码你会发现,mbedtls 不只是"小",它的可裁剪性和模块边界设计才是真正值得抄作业的地方。

2. 项目定位与技术选型:轻量TLS栈到底轻在哪

2.1 从OpenSSL到mbedtls:内存占用相差一个数量级

OpenSSL 功能全,但它的代码结构面向桌面和服务器场景,内存池、线程模型、线程安全机制都按大内存假设来设计。mbedtls 的目标场景是 Cortex-M 这类嵌入式处理器,所以它在设计之初就做了几个关键取舍:

  • 不使用标准 I/O 做网络层,而是让用户提供send和recv回调,底层是 TCP 还是 AT 指令都不影响上层协议。
  • 内存全部显式管理,没有隐式堆分配,甚至可以把所有上下文放在静态内存里,避免 malloc 碎片。
  • 加密套件进行了模块化拆分,不需要的算法可以在编译期直接去掉,比如只用 PSK 就不编 RSA。

我实际对比过一次:同一台 Linux 机器上编译 OpenSSL 1.1.1 和 mbedtls 2.x,OpenSSL 的 libssl 和 libcrypto 加起来超过 1.5MB,而 mbedtls 把MBEDTLS_SSL_PROTO_TLS1_2、AES、SHA256、RSA 这些常用模块打开,全静态链接的二进制是 180KB 左右。放到 MCU 上,裁剪后拿到 60KB 不是玄学,是模块化编译的必然结果。

2.2 源码包目录导览:哪些是你真正要关注的

解开源码包后,首先不要急着看代码,先摸清目录结构。mbedtls 的根目录和别的 C 库项目不太一样,它把"库代码"和"测试/示例"分得很清楚:

mbedtls ├── include/ │ ├── mbedtls/ │ │ ├── ssl.h # TLS 协议对外 API │ │ ├── x509_crt.h # 证书解析与校验 │ │ ├── ctr_drbg.h # 伪随机数发生器 │ │ ├── entropy.h # 熵源抽象 │ │ └── ... │ └── psa/ # PSA Crypto API 封装(新版才有) ├── library/ │ ├── ssl_tls.c # TLS 握手主逻辑 │ ├── ssl_cli.c # 客户端握手状态机 │ ├── ssl_srv.c # 服务端握手状态机 │ ├── ssl_msg.c # 记录层收发 │ ├── x509_crt.c # 证书链解析 │ ├── aes.c / sha256.c / rsa.c ... │ └── ... ├── programs/ │ ├── ssl/ # 官方示例:ssl_client1、ssl_server 等 │ ├── test/ # 单元测试和自测程序 │ └── util/ # 小工具,如 pk_verify ├── configs/ │ ├── config-mini.h # 最小配置模板 │ └── config-ccm-psk-tls1_2.h ├── tests/ └── CMakeLists.txt

这个目录结构里,include/mbedtls是库的公共头文件,library是具体实现。你二次开发时,绝大部分改动都发生在library下的同名文件里,而不是programs里的示例程序。要改协议行为,去ssl_tls.c和ssl_cli.c/ssl_srv.c;要改密码算法性能,去aes.c和对应的汇编优化目录。

2.3 功能模块梳理:TLS/DTLS/X.509/加解密

mbedtls 并不是一个只有 TLS 的库,它实际包含四层能力:

第一层是对称加密和哈希,比如 AES、ARIA、Camellia、ChaCha20、SHA-1/SHA-256/SHA-512。这些算法既被 TLS 记录层使用,也可以单独暴露给应用层做数据加解密。第二层是公钥算法,RSA、ECDSA、ECDH、Ed25519 这些,主要用于证书签名验证和密钥交换。第三层是 X.509 证书解析和校验,包括读取 DER/PEM 格式的证书,构建证书链,校验有效期和签名。第四层才是真正的 TLS/DTLS 协议实现,它依赖前三层,但协议流程本身是独立代码。

常见的错误是只把 mbedtls 当"一个加密库"来用,直接调aes_crypt_ecb来加密业务数据。这样虽然能用,但丧失了 TLS 层带来的会话管理、重传处理、密钥协商能力。我一般建议:只要是两个节点之间传输数据,优先用 TLS 层接口,而不是裸加密接口;只有本地存储加密才用底层对称加解密 API。

3. 编译与运行:把源码包在本地和交叉环境跑起来

3.1 本地快速编译:一把make过的舒服配置

在 Linux 环境下,mbedtls 的默认配置是可以在几秒内完成编译的。源码包自带 Makefile,直接执行就行:

cd mbedtls make -j4

默认会生成library/libmbedcrypto.a、libmbedtls.a、libmbedx509.a三个静态库以及programs下的可执行文件。第一次编译不建议改任何配置,先把默认编译跑通,确认工具链没问题,再考虑裁剪。

如果你不想用 Makefile,也可以用 CMake:

mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4

CMake 方式的好处是方便生成 IDE 工程,以及通过-DMBEDTLS_CONFIG_FILE=...指定自定义配置文件。两种方式生成的库完全一样,区别只在构建系统的可维护性。我一般在自己维护的项目里用 CMake,因为能更好地处理依赖关系。

3.2 交叉编译到ARM板:CMake和改config.h两种姿势

交叉编译时,最直接的方式是在 CMake 里指定工具链。假设你用一个 arm-none-eabi 工具链,可以这样:

cmake .. \ -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-gcc-toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DENABLE_PROGRAMS=OFF

这里ENABLE_PROGRAMS=OFF很关键,它关闭示例程序的编译,只生成库文件。嵌入式场景通常只需要静态库,不要示例程序,因为示例程序依赖操作系统 socket 接口,在裸机上根本链接不过。

如果用的不是 CMake 兼容的工具链,还有一个更通用的办法,直接修改根目录的Makefile,把CC变量改成交叉编译器路径:

make CC=arm-none-eabi-gcc AR=arm-none-eabi-ar

需要注意,改CC只影响编译,库的内部代码仍然要按目标平台的字节序来。mbedtls 默认在配置阶段会探测字节序,如果交叉编译时探测不到,就需要在config.h里显式定义MBEDTLS_ARCH_IS_ARM32或MBEDTLS_ARCH_IS_ARM64,告诉它目标架构,避免运行时字节序判断出错。

3.3 跑通最小HTTPS客户端:从握手到收发一条消息

源码包的programs/ssl/ssl_client1.c是一个最简单的 HTTPS 客户端 demo。它做的事情就四步:初始化上下文、设置证书和随机数、建立 TCP 连接、执行 TLS 握手后收发数据。我把它精简了一下,形成可复现的最小代码:

#include "mbedtls/net_sockets.h" #include "mbedtls/ssl.h" #include "mbedtls/entropy.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/x509_crt.h" static mbedtls_net_context server_fd; static mbedtls_ssl_context ssl; static mbedtls_ssl_config conf; static mbedtls_x509_crt cacert; static mbedtls_entropy_context entropy; static mbedtls_ctr_drbg_context ctr_drbg; int main(void) { // 1. 初始化各个上下文对象 mbedtls_net_init(&server_fd); mbedtls_ssl_init(&ssl); mbedtls_ssl_config_init(&conf); mbedtls_x509_crt_init(&cacert); mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); // 2. 加载 CA 证书用于校验服务端 mbedtls_x509_crt_parse_file(&cacert, "cacert.pem"); // 3. 熵源和伪随机数种子绑定 mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0); // 4. 建立默认 TLS 配置 mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED); mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL); mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg); mbedtls_ssl_setup(&ssl, &conf); mbedtls_net_connect(&server_fd, "example.com", "443", MBEDTLS_NET_PROTO_TCP); mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, NULL, mbedtls_net_recv); // 5. 握手 while ((ret = mbedtls_ssl_handshake(&ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) break; } // 6. 发送 HTTP 请求 char request[] = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; mbedtls_ssl_write(&ssl, request, strlen(request)); // 然后 read 响应... }

这段代码里,mbedtls_ssl_config_defaults是核心入口,它根据你是客户端还是服务端、使用 TCP 还是 DTLS,自动填充一套安全的默认参数。MBEDTLS_SSL_VERIFY_REQUIRED表示必须验证证书链,没有合法证书会握手失败,这能防止中间人攻击。mbedtls_ssl_set_bio是关键绑定:第一个参数server_fd是网络上下文,后三个参数分别是发送函数、接收函数和接收超时函数,你可以完全替换成自己的底层驱动接口,这也是 mbedtls 能跑在各种 RTOS 上的原因。

4. 核心模块拆解:TLS握手从进入到返回的完整链路

4.1 上下文初始化:ssl_st和ssl_config各管什么

拿到 mbedtls 源码,最容易混淆的是mbedtls_ssl_context和mbedtls_ssl_config这两个结构体。它们是两层不同的东西:

  • mbedtls_ssl_config保存握手阶段之前的静态配置。比如证书信任链、随机数回调、会话重传次数、加密套件列表,这些在整个连接生命周期内不变,可以在一次配置后给多条连接复用。
  • mbedtls_ssl_context保存一条具体 TLS 连接的所有运行时状态。比如当前握手状态、输入输出缓冲、协商出来的密钥、对端证书。它是会话级的,每个 TCP 连接都要有一个独立的 context。

在ssl_tls.c里,mbedtls_ssl_setup会把ssl和conf绑定到一起,并把ssl里缺失的状态全部初始化。常见的翻车点是:一种场景给多条连接共用同一个ssl_context,结果并发握手时状态互相踩。正确做法是每个连接一个mbedtls_ssl_context,所有连接可以共用同一个mbedtls_ssl_config。

4.2 握手状态机:从ClientHello到Finished的状态迁移

TLS 握手状态机是ssl_cli.c里最值得读的部分。它不像别的实现用一个大 switch 硬编码所有状态,而是用一个state字段标记当前步骤,然后每一步对应一个处理函数。我摘录了客户端握手的主流程片段:

int mbedtls_ssl_handshake_client_step(mbedtls_ssl_context *ssl) { // 上一状态已处理完成,切换到下一状态 if (ssl->state == MBEDTLS_SSL_HELLO_REQUEST) { // 发送 ClientHello ret = mbedtls_ssl_write_client_hello(ssl); ssl->state = MBEDTLS_SSL_SERVER_HELLO; } else if (ssl->state == MBEDTLS_SSL_SERVER_HELLO) { // 解析 ServerHello,协商版本和加密套件 ret = mbedtls_ssl_parse_server_hello(ssl); ssl->state = MBEDTLS_SSL_SERVER_CERTIFICATE; } else if (ssl->state == MBEDTLS_SSL_SERVER_CERTIFICATE) { // 解析并校验对端证书 ret = mbedtls_ssl_parse_certificate(ssl); ssl->state = MBEDTLS_SSL_SERVER_KEY_EXCHANGE; } // ... 后续还有密钥交换、证书验证请求、完成握手 return ret; }

这个状态机的设计核心是"每步只做一件事,并且非阻塞"。mbedtls_ssl_handshake在外层循环里反复调用mbedtls_ssl_handshake_step,如果底层 socket 没有数据可读,就返回MBEDTLS_ERR_SSL_WANT_READ,等下次事件循环再继续。这种非阻塞设计让 mbedtls 能轻松对接 RTOS 的事件驱动模型,而不是傻等一个网络包。

MBEDTLS_SSL_SERVER_CERTIFICATE到MBEDTLS_SSL_SERVER_KEY_EXCHANGE这一步,取决于密钥交换算法。如果使用 ECDHE,服务器会额外发送ServerKeyExchange消息;如果是静态 RSA 密钥交换,这一步会被跳过。读懂这个状态机,你就知道为什么在抓包时会看到某些 TLS 消息有时候有、有时候没有。

4.3 证书校验与信任链:为什么裸连自签证书会失败

x509_crt.c里实现了一个很典型的证书链构建与校验算法。核心函数是mbedtls_x509_crt_verify_chain,它会递归地检查每个证书的签发者是否和上一级的主题匹配。简化逻辑如下:

static int x509_crt_check_parent(mbedtls_x509_crt *child, mbedtls_x509_crt *parent) { // 检查 child 的 issuer 是否等于 parent 的 subject if (!x509_name_cmp(&child->issuer, &parent->subject)) { // 检查签名是否真的是 parent 签的 if (mbedtls_pk_verify(&parent->pk, child->sig_md, child->tbs.p, child->tbs.len, child->sig.p, child->sig.len) == 0) { return 0; // 验证通过 } } return -1; }

参数说明:child->issuer是当前证书的签发者字段,parent->subject是上级证书的主题字段,两者必须严格相等(DN 匹配)。child->sig是当前证书的数字签名,用parent->pk里的公钥来验签。这个机制保证了信任链是逐级签名、不可伪造的。

这里就是很多人跑自签证书失败的地方:服务端用的是自签证书,客户端没有把该证书加入到信任根,证书链无法构建,所以握手在证书校验阶段直接返回MBEDTLS_ERR_X509_CERT_VERIFY_FAILED。解决方法是把自签证书复制到客户端并调用mbedtls_x509_crt_parse_file加载成 CA 证书,或者用mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_NONE)跳过校验——但生产环境千万别用后者。

4.4 内存池与I/O回调:无OS环境下的适配点

mbedtls 自己管理输出和输入缓冲,默认缓冲区大小是 16KB,其中输入缓冲占据了连接生命周期的大部分内存。在内存紧张时,可以修改MBEDTLS_SSL_IN_CONTENT_LEN这个宏。常见做法是 TLS 记录最大长度为 16KB,但实际业务只有 2KB 的请求,这时可以把它改成 4096,节省 12KB RAM。

I/O 回调的适配是移植关键。mbedtls_ssl_set_bio传的mbedtls_net_send和mbedtls_net_recv只是默认实现。无 OS 的裸机上,SDK 通常提供at_send()和at_recv(),把它们替换进去就行:

static int sdk_send(void *ctx, const unsigned char *buf, size_t len) { return at_send_byte_stream(ctx, buf, len); // 返回实际发送字节数 } static int sdk_recv(void *ctx, unsigned char *buf, size_t len, uint32_t timeout) { return at_recv_byte_stream(ctx, buf, len, timeout * 1000); // 平台超时单位可能是us } mbedtls_ssl_set_bio(&ssl, &sdk_handle, sdk_send, NULL, sdk_recv);

注意sdk_recv超时参数的单位是毫秒,mbedtls 内部调用时传timeout值,如果你平台接口要求微秒,就要乘 1000。这里踩过坑的人不少:因为MBEDTLS_ERR_SSL_WANT_READ和MBEDTLS_ERR_SSL_TIMEOUT是两个不同错误码,如果超时处理不当,非阻塞握手的重传逻辑会认为对端没回复,从而错误重传,导致反复超时。

5. 避坑指南:编译和移植mbedtls的真实踩坑记录

5.1 随机数种子没提供:握手直接卡死在ClientHello

现象:服务端程序起来后,客户端连接进来,抓包只有 TCP SYN、SYN+ACK,没有 TLS 的 ClientHello 发出。原因:mbedtls_ctr_drbg_seed没被调用,或者熵源回调返回失败,导致随机数生成器不可用;而mbedtls_ssl_write_client_hello里要生成随机数,才会卡住不往下走。解决:在mbedtls_ssl_config_defaults之后、mbedtls_ssl_setup之前,检查mbedtls_ctr_drbg_seed的返回值;在桌面 Linux 上必须绑定mbedtls_entropy_func,在 MCU 上要提供硬件随机数接口。从那以后我每次搭 TLS 环境,都会先打印ctr_drbg_seed的返回码,而不是默认忽略。

5.2 默认配置没关调试输出:flash被日志吃光了

现象:交叉编译后固件体积比预期多了 100KB,而且运行时有大量调试信息从串口刷出去。原因:config.h里默认MBEDTLS_DEBUG_C开启,并且programs里用了mbedtls_debug_set_threshold,它会把每一条 TLS 内部消息都打印出来。解决:在正式发布配置里把MBEDTLS_DEBUG_C关掉,或者把调试阈值设置为 0。我一般会在config.h的末尾追加一个config_release.h,在里面统一#undef MBEDTLS_DEBUG_C,避免每次改完总配置污染公共代码。

5.3 字节序与对齐:在RISC-V上跑AES时报错

现象:AES 加密的数据在 ARM 上是好的,换到某 RISC-V MCU 后解密结果偶尔不对,还偶发 HardFault。原因:mbedtls 在某些平台默认用一个MBEDTLS_AES_USE_HARDWARE的宏走硬件加速,但硬件模块的字节序和软件实现不一致;另外从 Flash 读取可能按 32 位对齐,而 mbedtls 内部用了uint32_t *强制转换非对齐缓冲。解决:在config.h里明确#define MBEDTLS_NO_UDBL_DIVISION,并关闭不用的硬件加速宏;遇到访问非对齐地址时,用memcpy来代替强制指针转换。这个属于平台相关的玄学,排查方式就是逐个算法跑官方programs/test/test_suite_aes,定位是哪个算法触发异常。

5.4 证书解析卡在“Invalid PEM”:CRLF惹的祸

现象:在 Windows 上生成的证书文件传给 Linux 编译的客户端,mbedtls_x509_crt_parse_file返回MBEDTLS_ERR_X509_INVALID_PEM,在 Ubuntu 上又正常。原因:Windows 文本文件默认\r\n换行,而 mbedtls 的 PEM 解析器要求严格的\n结尾,遇到\r会误判标签结束。解决:用dos2unix转换证书,或者在代码里读文件后移除\r再传入mbedtls_x509_crt_parse。这个坑并不深,但排查起来很磨人,因为错误提示非常笼统。从那以后我写证书加载函数时,会先做一个字节级清理再解析。

5.5 低内存设备上握手失败:减少握手重传窗口

现象:在 192KB RAM 的设备上,TLS 握手偶尔成功,偶尔在Certificate消息后报MEM_ALLOC_FAILED。原因:证书链解析需要临时存储 DER 格式的证书和公钥对象,握手缓冲 16KB,再加上 RSA 解密要 4KB 以上,导致内存碎片。解决:优化配置MBEDTLS_SSL_IN_CONTENT_LEN到 4096,并关闭不必要的加密套件,比如删除 CBC 家族只留 GCM;另外把MBEDTLS_MEMORY_BUFFER_ALLOC_C打开,用一个堆外静态缓冲池统一管理 mbedtls 的分配,减少碎片对系统堆的影响。

6. 二次开发:按你的场景裁剪并验证改动

6.1 替换熵源:把硬件TRNG接进去

mbedtls 的熵源抽象是一个函数指针,mbedtls_entropy_add_source可以把你的硬件随机数接口注册进去。我一般会在系统启动后这样接:

static int hw_trng_poll(void *data, unsigned char *output, size_t len, size_t *olen) { hw_rng_read_bytes(output, len); // 平台API *olen = len; return 0; } mbedtls_entropy_add_source(&entropy, hw_trng_poll, NULL, MBEDTLS_ENTROPY_MAX_SOURCE_SIZE);

注意MBEDTLS_ENTROPY_MAX_SOURCE_SIZE定义的是每次该源能贡献的最大熵字节数,设太大会导致强制熵值不足,设太小又浪费不必要调用。经验值是 64,和默认的MBEDTLS_ENTROPY_BLOCK_SIZE匹配。

6.2 裁剪加密套件:只留PSK减少体积

如果你做的是私有协议设备间通信,根本不需要证书链,这时可以走 PSK 模式。配置顺序是:在mbedtls_ssl_conf_psk里设置预共享密钥,然后通过mbedtls_ssl_conf_ciphersuites传入你保留的套件 ID 数组。这样编译时MBEDTLS_X509_CRT_PARSE_C等证书相关模块会被裁剪,体积直接减半。

6.3 加一个自定义日志回调:定位线上问题

线上设备出现握手失败,尤其需要看到内部错误码。mbedtls 自带调试回调只输出到 stdout,我会在应用中自己实现mbedtls_ssl_set_log回调,把日志打到环形缓冲区,配合远程诊断接口读取。这样不用每次出问题都复现环境,直接看现场日志就能判断是证书过期还是加密套件不匹配。

整个改动流程我建议这样走:先在本地跑通programs/ssl/ssl_client1,再改配置裁剪,最后替换 I/O 层和熵源,每一步都跑一次官方测试用例验证,避免改动叠加后问题隐藏。验证方面,除了官方tests/test_suite_*,我还习惯用抓包工具对比握手包,确认 ClientHello 里携带的套件顺序符合预期。

希望这套二次开发的顺序能帮到你。做 TLS 底层的移植,说到底就是一个不断"裁剪 + 验证"的循环;从那以后我每次换平台,都强制先编译官方示例再动业务代码,至少能省掉一半的排障时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询