ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

mbed TLS深度实践:从源码结构到交叉编译与工程化落地

mbed TLS深度实践:从源码结构到交叉编译与工程化落地 做过嵌入式或者物联网设备开发的朋友一定绕不过一个现实问题设备要联网通信要加密证书要校验而设备本身的资源又紧巴巴的。在这个场景下mbed TLS 基本上就是“默认选项”级别的存在。这个库最初叫 PolarSSL后来被 Arm 收编改名叫 mbed TLS再后来长期维护在 TrustedFirmware 组织下面但它始终以 C 语言实现、以嵌入式友好著称。我接触它的时间不算短从最早的 1.3 系列一路用到现在的 3.x期间踩过不少坑也摸清了一些源码和工程上的门道。这篇不是带你逐行读注释的入门讲义而是结合我自己在实际项目里对 mbed TLS 做二次开发、交叉编译、测试和工程化落地的经历把这个库从 “能用” 到 “好用” 中间那些关键节点拆开讲清楚。适合这些人看准备在 Arm 平台或 MCU 上集成 TLS 的嵌入式工程师需要把 mbed TLS 集成进自己构建系统的平台软件工程师以及想弄明白一个成熟开源 C 项目是怎么组织代码、测试和工程治理的同学。1. 源码里的“三驾马车”library、include、tests拿到 mbed TLS 源码之后第一件事不是急着 make而是先把目录结构看懂。整个源码仓库摊开看核心就三块library、include和tests外加programs、scripts这些辅助目录。这三块的职责划分得很清楚理解它之后你再看任何具体模块都会顺手很多。1.1 library 目录真正的 C 实现都在这里library目录下放的是所有.c文件它们是 mbed TLS 的主体。你可以看到ssl_tls.c、ssl_tls13.c、x509_crt.c、aes.c、sha256.c、ecp.c这类文件。命名基本就是模块名找东西非常直观。如果做裁剪或者排查问题需要频繁在这里定位函数实现。一个容易被忽略的点是这里有些文件是按功能域拆得非常细的。比如 SSL/TLS 部分不止ssl_tls.c还有ssl_msg.c、ssl_srv.c、ssl_cli.c、ssl_ticket.c这些文件。这种拆分不是为了好看而是为了让开发者按需编译。当你不需要服务端功能时可以直接把ssl_srv.c从构建列表里拿掉减小固件体积。1.2 include/mbedtls配置头文件的枢纽地位include/mbedtls下面最核心的文件是build_info.h和mbedtls_config.h。在我用的 3.x 版本里mbedtls_config.h就是以前 2.x 时代的config.h它承担了全局编译开关的角色。这个文件里绝大多数条目是宏定义例如MBEDTLS_SSL_TLS_C、MBEDTLS_AES_C、MBEDTLS_ECP_DP_SECP256R1_ENABLED等。你在裁剪库功能时改的就是这些宏。整个库有几百个宏但日常工程中你经常碰到的也就几十个。关键在于理解这些宏的“依赖关系”比如你要开MBEDTLS_ECDHE_ECDSA_C它通常会依赖MBEDTLS_ECDH_C、MBEDTLS_ECP_C这些底层实现如果只开前者不开后者构建期大概率会报 undefined reference。我在实际项目里维护过一份裁剪得很狠的配置当时为了把固件体积压下来逐个宏去试依赖关系。后来学聪明了直接用scripts/config.py这个脚本去开关宏和检查依赖比人肉翻头文件靠谱得多。1.3 tests 目录分层清晰的测试体系tests目录不是随便堆测试脚本它的分层逻辑非常清楚。最顶层是tests/suites里面全是test_suite_*.function和test_suite_*.data成对文件。.function文件是测试逻辑的 C 代码骨架.data文件是参数化的测试数据。两者配合起来可以提供大量输入组合的测试场景。tests/scripts下有一些辅助脚本比如check-generated-files.sh用来检查生成文件是否与当前源码一致。这些脚本对 CI 非常有用。除了单元测试programs目录还提供了一堆可执行示例程序例如ssl_client1、ssl_server。这些不只是 demo它们本身就是很好的集成测试工具。我在交叉编译后经常拿programs/ssl/ssl_client1去连一个标准 HTTPS 服务验证目标板上 TLS 握手是否正常。2. C 语言实现里的“性格”上下文结构体、错误码和回调作为一个 C 语言库mbed TLS 的编码风格和设计习惯决定了你扩展它、调试它时的体验。这一节我把几个关键的实现特点讲透这些才是你真正写业务代码时天天打交道的部分。2.1 上下文Context结构体驱动一切mbed TLS 大量使用“上下文结构体”来承载状态。以 TLS 为例握手过程中需要保存客户端随机数、服务端随机数、会话密钥、证书链、握手摘要等状态这些不可能放在全局变量里嵌入式环境下并发和多实例太常见了。所以你会看到mbedtls_ssl_context这个大型结构体它内部又包含mbedtls_ssl_config配置、mbedtls_ssl_handshake_params握手状态、mbedtls_ssl_session会话等子结构体。这种设计的直接好处是“可重入”——同一个库代码可以同时服务多个 SSL 连接。你在写服务器程序时每个连接有各自独立的mbedtls_ssl_context互不干扰。坏处是结构体比较深调试时看变量会比较费劲。我的习惯是直接在 IDE 里展开结构体或者用 GDB 的print命令定位关键字段。2.2 错误码的“负值语义”和分层编号mbed TLS 的错误码设计初看不起眼实际用起来非常有用。所有错误码都是负值0 表示成功正值保留给上层使用。这个设计让你可以写这样的判断int ret mbedtls_ssl_handshake(ssl); if (ret ! 0) { // 处理错误 }错误码不是随便定义的它内部做了分层编号。高位代表模块低位代表具体错误。比如MBEDTLS_ERR_SSL_ALLOC_FAILED和MBEDTLS_ERR_AES_INVALID_KEY_LENGTH你一眼就能从名字看出模块归属。排查问题时我会用mbedtls_strerror()把错误码转成可读字符串这个函数在调试嵌入式设备时简直是救命稻草。2.3 回调函数是嵌入式定制的灵魂mbed TLS 不像 OpenSSL 那样把 I/O 抽象成 BIO而是提供了一组回调函数接口由你来实现底层收发。这意味着你可以把 TLS 跑在 TCP socket 上也可以跑在 AT 指令的 Modem 上甚至可以跑在自定义的总线协议上。在使用回调时有个细节要特别注意超时处理。如果你在 RTOS 环境下使用mbedtls_ssl_set_bio()注册了自定义的f_recv回调那么你必须自己保证这个回调在超时后返回MBEDTLS_ERR_SSL_WANT_READ而不是一直阻塞。这个坑我踩过一开始直接把 socket 的recv阻塞调用塞进去结果 TLS 握手时线程卡死排查了半天才发现是回调的阻塞行为破坏了 SSL 状态机的推进逻辑。3. 构建工程CMake、Makefile 和 Arm 交叉编译的实战取舍你用 mbed TLS 不只是为了在 PC 上把单元测试跑起来最终是要把它集成到嵌入式工程里。这一节讲构建我会明确区分“库本身怎么构建”和“怎么把它编进你的目标固件”这两件事。3.1 库本身的构建路径CMake 是主流Makefile 是老派选择mbed TLS 官方同时维护 CMake 和 Makefile 两套构建。CMake 是现在社区实际使用的主流尤其是当你的工程本身就是用 CMake 管理的时候用add_subdirectory把 mbed TLS 源码直接引进来很方便。CMake 构建的最简流程是这样的mkdir build cd build cmake .. make但这只是“能编过”不是“能用好”。实际工程里我会在 CMake 阶段传入关键变量cmake -DCMAKE_BUILD_TYPERelease \ -DCMAKE_TOOLCHAIN_FILE../toolchain-arm-none-eabi.cmake \ -DENABLE_TESTINGOff \ -DENABLE_PROGRAMSOff \ ..其中ENABLE_TESTING和ENABLE_PROGRAMS这两个开关决定了要不要编译测试套件和示例程序。对我而言库本身的构建阶段通常关掉它们以加快交叉编译速度专门的测试阶段再打开。传统 Makefile 路径适用于那些还没迁移到 CMake 的 Legacy 工程。直接用make会构建出library/libmbedtls.a、libmbedx509.a和libmbedcrypto.a这三个静态库。这三个库的划分对应 mbed TLS 从 2.x 延续至今的模块化思想静态库覆盖内容典型依赖场景libmbedcrypto对称加密、哈希、RSA/ECC、随机数、HKDF 等只需要加密算法时libmbedx509证书解析、证书链验证、CSR处理证书但不跑 TLS 时libmbedtlsSSL/TLS 协议栈完整 TLS 握手与记录层这个划分很实用。很多时候你的产品只需要做证书解析或者只需要做 AES-CCM 加解密那完全不需要链接整个 TLS 库。3.2 交叉编译到 Arm从工具链到 sysroot 的一整套链路交叉编译是嵌入式集成的重头戏。目标平台不同工具链也不同。最常见的两类Arm Cortex-M/M33 这类 MCU常用arm-none-eabi-gcc跑的是裸机或 RTOS没有操作系统提供的完整文件系统。Cortex-A 系列跑 Linux常用aarch64-linux-gnu-gcc目标系统有完整的 POSIX 环境。工具链正确还不一定够关键是头文件、库依赖sysroot要对得上。以 Cortex-A 的 Linux 环境为例cmake -DCMAKE_C_COMPILERaarch64-linux-gnu-gcc \ -DCMAKE_SYSTEM_NAMELinux \ -DCMAKE_SYSTEM_PROCESSORaarch64 \ -DCMAKE_BUILD_TYPERelease \ ..这里有个容易被忽略的点CMAKE_SYSTEM_NAME一旦设置为 LinuxCMake 会尝试探测 sysroot。如果你的交叉编译环境没有正确设置CMAKE_SYSROOT就可能链接到宿主机的 libc导致运行时崩溃或者链接器报错。处理办法是显式指定-DCMAKE_SYSROOT/path/to/your/rootfs -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAMNEVER对于 Cortex-M 裸机环境事情更严格一些。你几乎没有现成的 sysroot 可用标准库也是精简版或新库newlib/nano newlib。mbed TLS 在编译时需要对齐你的堆内存分配方式通常要自己实现mbedtls_platform_set_calloc_free()或者在配置头文件里定义MBEDTLS_PLATFORM_MEMORY宏。3.3 裁剪宏对二进制体积的影响到底有多大很多人关心剪裁之后能省多少 Flash我拿一个具体项目举例。当时用的开发板是 Cortex-M4基础配置完整 TLS 1.2 客户端 常见 ECC 曲线 AES-GCM编出来的libmbedtls大约 80 KB 级别的代码量。裁剪掉MBEDTLS_SSL_SRV_C不做服务端、MBEDTLS_SSL_DTLS不做 DTLS、MBEDTLS_X509_CRL_PARSE_C不解析吊销列表、去掉几个不需要的椭圆曲线之后体积能压缩到 50 KB 以下同时静态 RAM 占用也下降了。具体数字会随编译器和优化选项浮动。我的经验是不要凭感觉裁一定要保留一份“完整可运行”的基准构建然后每次裁剪几个宏跑一遍测试和功能自测量化记录体积变化。这套流程看起来笨但非常有效能避免某次裁剪引入的隐性问题在项目后期才爆发。4. 测试策略单元套件、互操作测试与真机验证一个加密库如果没有像样的测试你根本不敢把它用在产品里。好在 mbed TLS 自身的测试体系做得相当完整我们在集成过程中要做的不是另起炉灶而是理解这套体系并把它纳入自己的质量门禁。4.1 test_suite_* 的结构和运行方式这套测试框架是基于“功能函数 参数数据”的非常像数据驱动测试。test_suite_aes.function里写好了各种测试逻辑函数例如 AES 加解密、带 AEAD 的 GCM/CCM 模式。test_suite_aes.data则提供了成百上千组测试向量每一行代表一次用例执行包括输入、密钥、预期输出。在源码构建时如果ENABLE_TESTING打开CMake 会为每个 suite 生成一个可执行文件。你可以直接跑./tests/test_suite_aes它会全量运行该 suite 下的所有测试向量输出 PASS/FAIL。这套框架兼容在交叉编译后放到目标板上运行只要目标平台有基本的标准输出。把test_suite_*拷到设备上跑是在真机上验证移植正确性的最快手段。4.2 怎么评估测试是否覆盖到了我们的“改造成果”如果你对 mbed TLS 打了补丁或者修改了配置头文件那么跑一遍全量测试非常必要但更关键的是看懂哪些测试会受到配置影响。比如你裁剪掉了MBEDTLS_SHA1_C那么所有依赖 SHA-1 的测试套件自然就不应该被编译或运行否则会报错。这时候不要硬着头皮修测试而要考虑是不是裁剪本身不合理。我建议的流程是拿到新版本源码后先用默认配置在 PC 上完整跑一遍官方测试套件建立基准再改成你的裁剪配置重新跑一遍对比哪些套件被禁用、哪些通过。这个基准对比在升级 mbed TLS 版本时尤其重要能帮你快速发现新版本的行为变化。4.3 与标准实现做互操作测试单元测试覆盖的是“库自己和已知向量是否一致”而互操作测试解决的是“我的设备和别人的设备能不能通话”。我常用的工具是 OpenSSL s_server 和 s_client。在 PC 上起一个标准 OpenSSL 服务端openssl s_server -accept 4433 -cert server.crt -key server.key -www然后用交叉编译出来的 mbed TLS 客户端程序连它./ssl_client1 server_addr127.0.0.1 server_port4433如果握手成功说明你裁剪后的 TLS 配置在基本流程上是兼容的。反过来你也可以用 mbed TLS 起 server用 OpenSSL 的 client 去连。这一正一反两种方向我每次改配置头文件之后都会跑一遍。它不复杂但能拦截掉大多数“裁剪过头导致握手协议不兼容”的问题。4.4 在 Arm 目标板上做验证的几个注意点目标板上跑测试和 PC 上有很大区别。第一个注意点是随机数。mbed TLS 在握手时需要高质量的随机数默认会调用平台相关的mbedtls_platform_entropy_poll。如果目标板没有硬件 TRNG或者你还没适配好可以临时用测试用的确定性随机数回调但仅限于开发和测试绝不能带到生产环境。第二个注意点是超时和看门狗。MCU 环境下如果测试用例里有预期失败或异常输入可能触发某些耗时很长的错误处理路径。如果在跑测试时有硬件看门狗在喂着很可能会被复位。把看门狗暂停或者测试程序启动后先禁狗是嵌入式测试的常见操作。第三个注意点是 RAM。某些测试套件会分配较大缓冲区比如 TLS 握手缓冲区默认 16 KB 可能在 MCU 上偏大。不加处理直接跑测试可能出现 allocation failed。这时要检查MBEDTLS_SSL_MAX_CONTENT_LEN和MBEDTLS_SSL_IN_CONTENT_LEN这类配置适当调小或者调整测试场景。5. 工程化治理版本锁定、静态分析和持续集成最后一个部分我想聊的不再是具体 API 和宏而是把 mbed TLS 当做一个长期维护的第三方依赖怎么把它管好。这个环节做不好前面所有技术工作都会在后期的版本升级、安全补丁适配中消耗掉。5.1 版本锁定与安全公告跟踪mbed TLS 的版本迭代不算慢尤其是 3.x 系列出来后API 变化比较明显。如果是新项目建议直接用 3.x 最新 LTS 分支如果是老项目从 2.x 升级需要留出专门的迁移测试时间因为不少结构体字段和函数名都变了。版本锁定的意思是你的仓库里应该固定到你测试过的某个 tag而不是直接跟踪 master 或 development 分支。我在实际工程里会把 mbed TLS 作为 git submodule 或者 vendor 目录固定住同时在 CI 里记录它的版本号和当前 commit 号。这样任何一次构建都知道代码来自哪里排查问题时可复现性会好很多。安全公告一定要跟踪。如果 mbed TLS 发布了一个和你的使用场景相关的 CVE 修复你需要评估影响范围。这就要求你清楚自己用到了哪些功能模块否则只能“全民皆兵”地盲目升级。5.2 静态分析和编译告警怎么纳入日常C 语言的未定义行为在加密库这种高要求代码里是不能容忍的。mbed TLS 自身用 GCC/Clang 的高告警级别编译很多常见错误在编译期就能发现。在集成的工程里我会额外加-Wall -Wextra -Werror并配合-fstack-protector-strong这类安全加固选项。如果条件允许用 Clang Static Analyzer 或者 cppcheck 定期扫一遍。有一次我改了一个自定义的熵源回调静态分析立刻提示了一个潜在的空指针解引用这在运行时可能非常隐蔽。静态分析工具虽然不是万能的但对这种老牌 C 项目来说投入产出比很高。5.3 CI 里怎么设计 mbed TLS 的测试阶段一个实用的 CI 流水线我会设置四个阶段构建编译 PC 版本编译目标平台的交叉编译版本。单元测试在 PC 上跑全量 test_suite_*。互操作测试用 OpenSSL 做双向握手测试。目标板冒烟测试通过对应工具把编译好的程序和测试套件部署到设备跑一小部分关键用例。这些阶段可以分属不同的流水线任务。日常 MR 可以只跑前两个阶段合并前或发布前再跑后两个阶段。这能在“反馈速度”和“质量关卡”之间取得平衡。我额外想提醒的一点是如果你们的 CI 使用容器环境跑交叉编译请记得把工具链也锁版本。arm-none-eabi-gcc 从 10 升级到 12 或者更高版本时部分优化行为有变化可能导致之前能过的测试出现 flaky。工具链锁定和 mbed TLS 版本锁定同样重要。我个人的习惯是每次拿到新版本源码都会保留一份“默认配置 全量编译 全量测试”的日志存档。这样后面做裁剪、升级或移植时随时有基准可对照。不要嫌麻烦工程化的本质就是把这些“费工夫”的步骤固化下来。你踩过的构建坑、测试坑也会在这些规范化流程里变得越来越少。
返回列表