资讯动态

mbedtls移植实战:从配置裁剪到熵源加固的完整避坑指南

发布时间:2026/10/14 3:15:23 来源:尧图企业网站定制
简介面向 ARM 架构芯片环境下的 mbedtls 移植体验资料尤其适合 armv9 平台开发人员。资源包仅有一个 PDF 文档大小约两百千字节便于快速阅读。内容按照实际操作顺序展开先从官方代码仓库获取源码再借助通用的工程模板快速生成测试工程接着介绍如何将 mbedtls 的核心库和头文件正确拷贝至工程内以及如何通过修改构建脚本指定交叉编译工具链。文档还详细说明了启用自测功能所需添加的编译宏并演示了在源文件中调用 SHA256 自测函数的方法。最后作者给出了在系统模拟器中编译、运行并验证该程序的具体过程。整份记录不仅列出了关键命令还解释了其中的配置意图和常见问题能够帮助读者避开移植过程中常见的路径、依赖和链接错误。对于正在研究安全启动、可信执行环境等 Arm 安全技术的工程师这份笔记的实践价值较高。目前已有三百三十人学习下载属于短小精悍的实战入门参考。1. mbedtls 移植体验轻量加密库不是拷进去就能跑的mbedtls 移植体验这件事做过的人大多经历过同一个转折最开始以为它就是个静态库拷进工程编一下链一下就算完紧接着就会在握手失败、内存爆掉、熵源报错、编译器告警里轮流怀疑人生。我先后在两个不同架构的 MCU 平台和一套 RTOS 上把 mbedtls 从源码跑到稳定建立 TLS 连接最大的感受是算法层反而不用操心真正磨人的是 mbedtls 跟目标平台之间的那层“胶水”——配置文件怎么裁、内存从哪来、随机数种子怎么喂、系统时间没有怎么办。这篇笔记讲的是这些实战细节覆盖从“这是什么”到“怎么做”再到“坑在哪”的完整过程适合正在评估 mbedtls 或者在移植路上卡壳的嵌入式开发者。2. 移植先看源码骨架config.h、平台层与构建脚本2.1 mbedtls 源码包的四个目录哪些动哪些不动拿到 mbedtls 源码压缩包先别急着往工程里拖。通用源码树分成四个区域include、library、programs、tests各自分工非常明确。include 下是全部头文件其中include/mbedtls/mbedtls_config.h是所有编译开关的汇集地你这趟移植的主战场就是它library 下是算法与协议实现典型规律是一个算法一个 C 文件aes.c、sha256.c、ssl_tls.c命名清楚到你翻目录就能猜到哪个文件管哪块programs 里是官方示例和自测程序别编进目标板tests 是单元测试基本只在主机环境跑。移植时真正要改的只有一个配置文件。library 下的源文件按“信任原版、链接期裁剪”的原则整目录编进去一个都别手工挑。原因后面细说。很多人在第一步就翻车是因为把 programs 里的ssl_client1.c这类示例代码当成库的一部分一起编了结果 CFLAGS 里缺宏、缺头文件路径告警刷了满屏还以为是库本身有问题。2.2 MBEDTLS_CONFIG_FILE 是移植的万能钥匙mbedtls 提供了一层配置覆盖机制编译时用-DMBEDTLS_CONFIG_FILE\my_config.h\就能把默认的mbedtls_config.h整个替换。我一般会为每个项目维护一个独立的配置文件而不是直接去改源码包里的默认文件。好处很实在以后升级 mbedtls 版本只需对比新旧默认配置不用去记自己在原文件里改过哪几行团队里多个产品线想要不同的裁剪组合时切换 CFLAGS 里的宏就能各编各的互不污染。你手里的配置会决定整个移植难度。以下这类宏分组心里要有数/* 平台层相关这组决定库怎么跟你的系统打交道 */ #define MBEDTLS_PLATFORM_C #define MBEDTLS_PLATFORM_MEMORY #define MBEDTLS_PLATFORM_TIME /* 功能开关决定能跑什么协议角色 */ #define MBEDTLS_TLS_CLIENT_C #define MBEDTLS_TLS_SERVER_C /* 算法开关决定代码体积和握手能否成功 */ #define MBEDTLS_AES_C #define MBEDTLS_SHA256_C #define MBEDTLS_ECDHE_C #define MBEDTLS_ECP_C配置文件的逻辑很直白定义某个宏代表启用对应模块注释掉代表不编译。常见做法是先照抄默认配置把目标板上用不到的宏关掉而不是每次从零写。我一般先把与网络 socket 相关的MBEDTLS_NET_C关掉因为裸机平台上这个模块基本没法用它的实现依赖 POSIX 的 socket 接口留着你迟早会踩编译错误。2.3 平台层三个开关少一个后面都折腾MBEDTLS_PLATFORM_C是所有平台回调的总开关这个宏不开mbedtls_platform_set_calloc_free、mbedtls_platform_set_time这一整套自定义回调 API 根本不会编译进库你后面想接自己的内存池和 RTC 都没地方挂。MBEDTLS_HAVE_TIME_DATE控制时间相关功能证书有效期校验依赖它。没有这个宏TLS 握手时证书验证环节拿不到当前时间要么跳过有效期检查不安全要么直接报证书错误。MBEDTLS_PLATFORM_MEMORY是自定义内存分配器的开关开了它才允许你用mbedtls_platform_set_calloc_free把库内部的calloc/free换掉。这里有个容易误会的点这个宏跟MBEDTLS_MEMORY_BUFFER_ALLOC_C不是一回事前者只是允许你注入自己的分配函数后者才是 mbedtls 自带的固定内存池实现两者可以配合使用但别混淆。3. 最小移植路线在裸机平台上让 TLS 客户端跑起来3.1 把 library 整目录编进库让链接器替你裁剪正确姿势是library目录下所有.c文件全部编进一个静态库算法和功能的取舍交给配置文件在编译期决定最终没有引用到的模块在链接阶段由垃圾回收段剔除掉。如果手工挑.c文件漏掉某个哈希模块的编译单位通常在链接时你会看到一个不明不白的符号缺失错误而且错误信息还不好定位。用通配符收集全部源文件是省事又稳的路子MBEDTLS_LIB_SRCS : $(wildcard $(MBEDTLS_DIR)/library/*.c) MBEDTLS_LIB_OBJS : $(patsubst %.c,%.o,$(MBEDTLS_LIB_SRCS)) board_mbedtls.a: $(MBEDTLS_LIB_OBJS) $(AR) rcs $ $^这段 makefile 的逻辑就是把 library 下所有 C 文件一一编译成目标文件然后用 ar 打包成静态库。参数说明里有个细节wildcard不递归子目录如果你的工程结构把源码摊开在不同目录得自己重新拼路径。另一个容易被忽略的参数是优化级别library下的编译建议不低于-O2像sha256.c这种循环密集的模块-O0和-O2的性能差距肉眼可见在低主频 MCU 上直接表现为握手耗时翻倍。CFLAGS 里必须带上配置文件指路-DMBEDTLS_CONFIG_FILEmbedtls_config_myboard.h注意嵌套引号的写法。如果你是 CMake 工程也可以直接target_compile_definitions(mbedtls PUBLIC MBEDTLS_CONFIG_FILEmbedtls_config_myboard.h)3.2 补齐三样运行时依赖时间、随机数、内存移植到裸机上最常见的问题是这三个东西不存在没有系统时间、没有硬件随机数、没有标准堆管理器。逐个解决并不复杂只是都得在初始化阶段提前做好。先解决时间。库内部通过mbedtls_platform_set_time挂载你自己的时间回调#include mbedtls/platform.h time_t board_get_time(void *ctx) { /* 从 RTC 驱动读取绝对时间戳单位是秒 */ return board_rtc_get_timestamp(); } /* 初始化阶段调用一次 */ mbedtls_platform_set_time(board_get_time);参数说明回调函数的void *ctx参数可以忽略库内部调用时传的是空指针返回值是标准的 Unix 时间戳。如果你的平台根本没有 RTC一个常见做法是在编译期定义固定的生产时间这样证书有效期校验能过只是证书过期后你得手动更新固件。再解决随机数。这一步是重头戏慢一点写。mbedtls 的随机数架构是双层底层是熵源负责收集不可预测的噪声上层是 CTR_DRBG负责把熵扩展成任意长度的随机序列。裸机平台上没有/dev/urandom所以得手动把硬件 RNG 或者 ADC 噪声接到熵源上/* 把硬件 RNG 接到 mbedtls 熵源的回调 */ static int board_hw_rng_pull(void *ctx, unsigned char *buf, size_t len) { /* 从片上 RNG 外设读 len 字节读满返回 0失败返回非 0 */ return board_rng_read_bytes(buf, len); } /* 初始化熵源和 DRBG */ mbedtls_entropy_add_source(entropy, board_hw_rng_pull, NULL, 128, MBEDTLS_ENTROPY_SOURCE_STRONG);参数里那个128是每次熵源回调被要求提供的字节数太小会导致 DRBG 反复调用熵源浪费时间太大在无硬件 RNG 的平台上会让回调压力很大一般取 64 到 256 之间。MBEDTLS_ENTROPY_SOURCE_STRONG是熵源强度标记表示这个源够可靠库内部会把它纳入默认熵池计算。如果你的硬件读一次产生不了那么多字节一个常用做法是连续读多次拼满缓冲区或者用 ADC 采样末尾几个有效位累积填充。内存方面如果平台堆空间充足直接用标准库的malloc即可啥都不用做。如果堆不够或者碎片严重就开启MBEDTLS_PLATFORM_MEMORY然后挂自己的内存池mbedtls_platform_set_calloc_free(pool_calloc, pool_free);这里容易踩的坑是你自定义的calloc必须真正把内存清零而且返回的地址务必按 8 字节对齐mbedtls 内部的联合体结构假设了自然对齐给你不对齐的块后患无穷。具体现象和排查放后面避坑章节里展开。3.3 最小化 TLS 客户端初始化骨架依赖都补齐之后初始化流程可以压成一段最小骨架。下面这段代码是 TLS 客户端连接前的标准初始化序列#include mbedtls/entropy.h #include mbedtls/ctr_drbg.h #include mbedtls/ssl.h #include mbedtls/x509_crt.h mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context drbg; mbedtls_ssl_context ssl; mbedtls_ssl_config conf; mbedtls_x509_crt cacert; void tls_client_init(void) { mbedtls_entropy_init(entropy); mbedtls_ctr_drbg_init(drbg); mbedtls_ssl_init(ssl); mbedtls_ssl_config_init(conf); mbedtls_x509_crt_init(cacert); /* 挂熵源 */ mbedtls_entropy_add_source(entropy, board_hw_rng_pull, NULL, 128, MBEDTLS_ENTROPY_SOURCE_STRONG); /* DRBG 种子设定 */ mbedtls_ctr_drbg_seed(drbg, mbedtls_entropy_func, entropy, NULL, 0); /* 设置默认参数客户端、流式传输、TLS1.2 预设 */ mbedtls_ssl_config_defaults(conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); /* 指定随机数生成器 */ mbedtls_ssl_conf_rng(conf, mbedtls_ctr_drbg_random, drbg); /* 先开可选验证方便抓包排查 */ mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_OPTIONAL); mbedtls_ssl_setup(ssl, conf); }这段代码的逻辑顺序是固定的先把所有上下文结构初始化为零态再挂熵源然后用熵源给 DRBG 做种子最后把 DRBG 交给 SSL 配置。mbedtls_ssl_config_defaults里的第三个参数MBEDTLS_SSL_PRESET_DEFAULT表示默认配置预设它会自动选择默认的 TLS 版本范围和安全级别。初始化之后连接阶段还需要设置服务器地址、端口、CA 证书链然后就是循环调用握手直到成功。这部分各家平台差异很大不展开但有一个建议第一版先把MBEDTLS_SSL_VERIFY_OPTIONAL开着跑通链路等日志能走通再改回MBEDTLS_SSL_VERIFY_REQUIRED。4. mbedtls 移植避坑实录五个高频问题与排查路径4.1 坑CTR_DRBG 初始化直接返回错误现象调用mbedtls_ctr_drbg_seed返回值不是 0打印错误码发现是MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED。原因熵源回调没有真正提供足够的数据或者返回值协议没遵守。很多平台 RNG 外设需要先上电初始化才能读如果你在board_hw_rng_pull里直接读一个未初始化的外设读出来的全是 0熵源认为采集失败。还有一种情况你加的熵源阈值填得比实际回调产出还大。解决先在board_hw_rng_pull里加一个简单的自检连续读 16 字节打印出来看是否变化。RNG 外设初始化必须在第一次回调之前完成放在tls_client_init里显式调用一次驱动初始化最稳妥。阈值参数临时改成 32降低熵源单次输出压力等跑通后再调回 128。4.2 坑自定义 calloc 导致随机崩溃现象用标准库 malloc 一切正常换成自定义内存池后在握手阶段随机进入硬错误中断而且每次崩的位置都不一样。原因自定义内存池没有保证 8 字节以上对齐。mbedtls 内部大量使用结构体联合体编译器假设其基址是自然对齐的你给了一个 4 字节对齐甚至只对齐 2 字节的块某些类型读取时直接触发总线错误。这类问题非常难查因为表现是随机的甚至跟优化级别相关。解决内存池的起始地址和每次分配的块首地址都要按sizeof(void *) * 2对齐也就是 64 位系统按 16 字节对齐。检查你的内存池分配头是否有保存长度字段如果有确保长度字段本身也没破坏对齐。另一点血泪经验在pool_calloc里必须用循环把块清零别用memset依赖外部的内存库万一你的memset实现也有对齐假设就麻烦了。4.3 坑TLS 握手总是断在证书验证之后现象握手日志走得通但每次都在服务器发送 Certificate 之后客户端回一个 alert连接断开。原因绝大多数情况是时间不对或者 CA 证书链没配完整。证书验证首先检查有效期你的 RTC 如果初始化出来是 1970 年 1 月 1 日那任何现代证书都过期了。另一种情况是你只加载了服务器证书没加载签发它的根证书验证链没法建立。解决先用MBEDTLS_SSL_VERIFY_OPTIONAL模式跑一遍看看返回值里有没有证书验证错误码MBEDTLS_ERR_X509_CERT_VERIFY_FAILED。如果是时间问题把board_get_time临时写死成当前编译日期跑通后再接 RTC如果是证书链问题用主机导出根证书为 PEM 格式通过配置的 CA 接口加载进去。一个惯用技巧调试阶段把验证模式设为 Optional同时打印验证结果标志位等定位之后再收严。4.4 坑库体积翻了好几倍Flash 不够用现象编出来的固件比预期大很多用size命令一看mbedtls 相关段占了 400KB 以上。原因默认配置把 TLS 1.3、所有曲线、所有哈希算法、调试模块全开了。很多模块在裸机平台上根本用不上但默认配置不懂你的场景它只会按最通用的方式把所有东西编进去。解决裁剪是移植的一部分不是事后优化。先关MBEDTLS_DEBUG_C这个模块在发布固件里没有任何价值再看你的服务端用的什么曲线通常只保留一条就能满足绝大多数场景。链接器记得打开--gc-sections让没被引用的函数段直接被丢弃。还有一点MBEDTLS_SSL_PROTO_TLS1_3这个宏在资源紧张的平台上可以先不启用TLS 1.2 在兼容性和资源占用上都更稳。4.5 坑TLS 1.3 在低内存平台上握手直接失败现象开了 TLS 1.3 后64KB RAM 的板子握手阶段报分配内存失败任务栈溢出。原因TLS 1.3 需要额外的会话票据、密钥调度状态、Early Data 相关缓冲区资源消耗比 TLS 1.2 高一个量级。低内存平台不是跑不了而是默认配置给的缓冲尺寸是按通用场景设计的。解决调入MBEDTLS_SSL_IN_CONTENT_LEN和MBEDTLS_SSL_OUT_CONTENT_LEN这两个宏把读写缓冲从默认的 16KB 压到 4KB 或者 2KB能省出巨大 RAM。如果服务端兼容良好直接定义MBEDTLS_SSL_MAX_CONTENT_LEN为 4096TLS 1.2 的握手消息大多数小于这个值。实在压不下来就在配置里退回只启用 TLS 1.2先把产品跑稳再说。5. 移植后的两道检验openssl 反测与体积裁剪5.1 用 openssl s_client 反向验证你的设备和证书移植后最有效的验证方式不是自己写客户端连自己的服务端那叫自娱自乐。反过来用 PC 上的标准 TLS 客户端去连目标板上跑起来的 mbedtls 服务端才能真正暴露问题。如果目标板做的是客户端也可以让目标板连 PC 上用 openssl 起的测试服务端这样能同时验证证书配置和双向握手。常用命令是# 起一个测试服务端监听 4433 端口 openssl s_server -accept 4433 -cert server.crt -key server.key -state # 或者用客户端视角连接你自己的板子 openssl s_client -connect 192.168.1.100:4433 -CAfile ca.crt -state参数说明-state会打印握手每个阶段的状态机切换对定位“卡在哪个消息之后”非常直观-CAfile指定验证对端证书用的 CA 文件调试时如果只是看链路通不通可以加-verify_quiet忽略证书告警。注意 s_server 默认要求客户端完成证书验证如果你的板子作为客户端没有配客户端证书需要在 s_server 那边加-verify_return_error并调整-Verify参数否则服务端会在 CertificateRequest 这一步卡住。5.2 跑 selftest 确认算法模块没编歪自测程序是 mbedtls 移植后的第一道安检它不需要网络、不需要服务器只验证相关模块能否正确加解密。在主机上把programs/test/selftest编译出来跑一遍当然有价值但更贴近目标板的做法是把 selftest 集成到你的板级测试代码里在初始化完成后调用对应模块的 self-test 函数#include mbedtls/aes.h #include mbedtls/sha256.h #include mbedtls/ecdh.h if (mbedtls_sha256_self_test(0) ! 0) { /* 日志输出SHA256 自测失败 */ } if (mbedtls_aes_self_test(0) ! 0) { /* 日志输出AES 自测失败 */ }参数说明mbedtls_sha256_self_test的参数是 verbose 标志传 1 会把详细对比数据打印出来发布固件里传 0 即可。自测的意义不仅是确认算法正确更重要的是确认你的自定义calloc、平台时间回调没有破坏库的底层假设很多对齐问题在自测阶段就会暴露不用等 TLS 握手才发现。我习惯在板子上电自检时就跑一遍这三个自测过了再进主逻辑。5.3 看 map 文件做裁剪别再用“感觉”定体积裁剪不看数据就是玄学。统计 mbedtls 在你目标板上到底占用多少空间最直观的办法是编译完后查看 map 文件定位.text段与.data段里各个目标的占用排行riscv64-unknown-elf-nm -S --size-sort build/app.elf | tail -30这条命令会按符号大小列出最后 30 个符号mbedtls 的大块头函数会排在最前面。参数说明-S表示打印符号大小--size-sort按大小升序排列tail -30取最大的那些核对。常见的大户集中在ecp_*.c的曲线运算函数、bignum.c的大数乘法组、ssl_tls.c的消息帧处理函数这些就是裁剪时最值得注意的目标。如果发现某个曲线运算函数占了十几 KB 但你根本不用那条曲线回配置文件里清掉对应的MBEDTLS_ECP_DP_*宏再编一次。有一个通用建议配置裁剪不要零敲碎打而是先清掉调试模块和用不到的协议版本再砍曲线和哈希最后再看体积达标没有。记住裁剪之后一定要把 selftest 重跑一遍有些算法模块之间存在隐式依赖砍过头会在握手时以“不支持”告警的方式惩罚你。6. 从“能握手”到“敢量产”内存策略与熵源加固这两关6.1 用 memory_buffer_alloc 锁死内存占用峰值堆分配在裸机平台上是个隐患碎片化带来的不确定性在格式化环境里还能忍但在联网设备上长时间运行的 TLS 重连会让堆碎片逐渐恶化。mbedtls 自带的MBEDTLS_MEMORY_BUFFER_ALLOC_C模式值得认真考虑。它做的事情很简单初始化时给你一块固定大小的静态缓冲之后的calloc/free都在这块缓冲内分配不会触碰系统堆。好处是完全可控不会因为别处的 malloc 毛刺让 mbedtls 分配失败#include mbedtls/memory_buffer_alloc.h static unsigned char mbedtls_pool[16 * 1024]; void tls_mem_pool_init(void) { mbedtls_memory_buffer_alloc_init(mbedtls_pool, sizeof(mbedtls_pool)); }参数说明缓冲大小的确定建议用实测法先跑通一次完整握手和一次重连通过mbedtls_memory_buffer_alloc_cur之类的接口读取峰值占用然后在这个峰值上再留 30% 余量。16KB 只是一个针对轻量客户端的经验参考值如果你启用了 TLS 1.3 并且保持默认 16KB 的 SSL 读写缓冲这个池根本不够需要另行增大。关键点这个初始化必须发生在任何 mbedtls 分配动作之前通常放在板级初始化最早的阶段。6.2 熵源加固多路混合与握手前自检随机数的质量决定了 TLS 握手的安全底线。硬件 RNG 并不总是完全可靠有些平台 RNG 外设在低电压或电磁干扰环境下会输出退化为固定模式的序列。mbedtls 的熵池架构允许挂多个源库内部会混合它们的结果这本身就是一种保护所以尽量别只挂一路源。一个常见做法是“硬件 RNG 高分辨率定时器噪声”组合mbedtls_entropy_add_source(entropy, board_hw_rng_pull, NULL, 128, MBEDTLS_ENTROPY_SOURCE_STRONG); mbedtls_entropy_add_source(entropy, board_timer_noise_pull, NULL, 32, MBEDTLS_ENTROPY_SOURCE_WEAK);第二路源的贡献不需要多大32 字节就够强弱标记用MBEDTLS_ENTROPY_SOURCE_WEAK表示。它存在的意义不是独立产生足够熵而是在主源退化时给熵池增加不确定性。初始化时还可以调用mbedtls_entropy_func自检一次如果返回错误说明熵池状态异常此时即使握手函数没报错也应该主动拒绝启动 TLS。这个习惯让我在产品发布前躲过一次危机某批板子在强电磁干扰环境下 RNG 输出出现周期性重复因为当时代码里只有单一熵源DRBG 种子质量下降直接导致握手随机数可预测。后来加入了定时器噪声混合源并把熵源自检加在启动流程里再没出现过同类问题。说到底mbedtls 移植跑通只是开始把每一个平台特定的假设都验证一遍才敢说能交付。希望这篇文章能帮你少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑