ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

mbedtls源码编译实战:从解压到Python调用与嵌入式移植

mbedtls源码编译实战:从解压到Python调用与嵌入式移植 简介mbedtls是一款轻量级的开源加密库使用C语言编写专注于实现安全套接层与传输层安全协议在嵌入式、物联网及移动应用领域得到广泛应用。这份源码包完整收录其核心代码适合需要深入理解超文本传输安全协议及加密通信底层机制的工程师学习。压缩包中共有1332个文件大小约13.47兆字节文件类型包括C源文件、头文件、数字证书文件、私钥文件、编译目标文件以及常用开发工具工程文件同时附带大量测试套件和命令行示例程序便于对照代码进行编译验证。该资源已有357人学习下载是同类源码分析资源中较受关注的一份。通过研读源码可以掌握较新版本的传输层安全协议实现、常见对称与非对称加密算法、X.509证书解析以及随机数生成等关键模块的工程做法随包附带的证书、密钥和测试工具还能帮助读者快速搭建实验环境为定制安全通信方案奠定基础。 拿到mbedtls源码.rar这种压缩包第一反应往往是源码有了然后呢嵌入式圈子里的老手都知道mbedtls 是 ARM 家的开源 TLS/SSL 协议栈前身叫 PolarSSL主打轻量、易裁剪、内存占用小特别适合跑在单片机、物联网模组这些资源有限的环境里。但源码摆在那儿真正能把它用起来的人并不多关键是很多人卡在了“第一步”上解压之后不知道从哪里下手编译不过去或者就算编过了也不知道怎么把里面的 AES、SHA256、RSA 这些算法拿出来给自己用。这篇文章就打算把我折腾 mbedtls 源码的真实过程记录下来。从解压后如何快速定位文件结构到怎么在嵌入式环境和桌面环境里分别编译再到最实用的一个场景——用 Python 的 ctypes 直接调用 mbedtls 编出来的动态库把 AEC、SHA256 这些算法当“外挂”用起来最后还会把我踩过的几个坑一并交代清楚。如果你是刚接触 mbedtls或者手头正好有一份源码包不知道从何下手这篇应该能帮你省不少时间。1. 拿到源码包之后的第一件事先泼一盆冷水直接把mbedtls源码.rar解压出来然后去找main函数是找不到的。这不是一个应用程序编译完也不是一个可执行文件它是一个库一个供你调用的加密工具集。本质上的工作方式是把里面各个模块编译成目标文件然后打包成静态库.a文件或者动态库.so/.dll再通过头文件的接口去调用。所以拿到源码包之后我建议你先干三件事解压、看 README、跑一次基础编译验证。不要一上来就急着改配置也不要先看某个具体算法的实现代码先把整个工程跑通一次建立起“源码能编译”的信心后面所有操作才有意义。1.1 这个压缩包装的到底是什么解压之后你会看到一个干净整洁的目录结构核心部分大概是这样的include/mbedtls/全部对外头文件都在这里你调用任何算法之前要#include的头文件都在这个目录里。library/各个模块的.c源码文件比如aes.c、sha256.c、rsa.c、x509.c等加密算法、证书解析、TLS 协议栈的实现全部在这里。programs/官方自带的一些测试程序比如aes/aescrypt2hash/hellossl/ssl_client1这些不是主菜但作为参考例子非常宝贵。tests/单元测试数据想验证某个算法对不对可以来这里跑测试。configs/包含若干现成的配置文件模板比如config-mini-tls1_1.h方便你针对不同场景定制配置。如果你下载的版本是 3.x 或者 2.x目录结构有些细节差异但整体上就是这样。值得注意的是mbedtls 2.x 和 3.x 的 API 变化比较大如果你是要移植老项目先确认好版本号很关键。2.28 是 LTS 长期支持版3.x 是最新主线两者代码风格上差异不小。1.2 为什么建议自己编译源码而不是直接用包管理器很多桌面开发环境可以直接用包管理器装 mbedtls比如 Ubuntu 上apt install libmbedtls-dev就完事了。但如果你做的是嵌入式开发几乎必然要自己用源码交叉编译原因有三点。第一嵌入式平台的交叉编译工具链和你在 PC 上用的完全是两回事。ARM Cortex-M 上跑的是 arm-none-eabi-gcc 或 armcc你要把 mbedtls 编译成能在 MCU 上跑的库必须用对应的工具链去编译源码第二mbedtls 的配置裁剪极度依赖源码里的mbedtls_config.h配置文件包管理器装好的库默认配置是按通用场景来的RAM 占用相对较大你往往需要手动关掉不需要的算法来节省内存比如只保留 AES-GCM 和 SHA256其他全关掉第三自己编译源码你才能调底层接口比如把内存分配函数换成你自己实现的池化分配这是做物联网设备逃不开的活儿。所以我的建议是即使你只是想在 PC 上快速验证一下也请从源码编译开始走一遍这样后面转交叉编译时思路是清晰的。2. 源码目录结构与核心模块这部分我挑几个重点模块来分析不是说让你把所有源码都通读一遍那是编译器该干的事。你需要做的是知道工具箱里有哪些工具每个工具大概在哪个抽屉里用到的时候能最快找到它。2.1 关键源码文件速查表我把高频用到的文件整理成了一张表方便你检索功能需求源码文件头文件AES 加解密library/aes.cinclude/mbedtls/aes.hSHA256 摘要library/sha256.cinclude/mbedtls/sha256.hRSA 加解密/签名library/rsa.cinclude/mbedtls/rsa.h大数运算library/bignum.cinclude/mbedtls/bignum.hTLS 客户端library/ssl_tls.c等include/mbedtls/ssl.h证书解析library/x509_crt.cinclude/mbedtls/x509_crt.hCTR-DRBG 随机数library/ctr_drbg.cinclude/mbedtls/ctr_drbg.h我平时用得最多的是 AES 和 SHA256这两个可以说是物联网数据传输的看门神。AES 用来做业务数据加密SHA256 用来做完整性校验和密钥派生。如果你做云端设备接入还会用到 HMAC、RSA 或 ECDSA 做签名文件也都在上面这个列表的辐射范围内。2.2 头文件里藏着的是 API 契约mbedtls 的设计风格和很多开源库不太一样它的 API 大量采用“初始化-使用-释放”三段式结构。比如要算一个 SHA256你至少会碰到这三个函数mbedtls_sha256_init()初始化上下文结构体。mbedtls_sha256_update()/mbedtls_sha256_finish()喂数据、取结果。mbedtls_sha256_free()释放资源。这种设计对嵌入式环境非常友好因为上下文结构体本身可以由调用者分配——你可以把它放在栈上也可以放在静态内存区完全避免了动态内存分配带来的不确定性。这也是 mbedtls 能在内存只有几十 KB 的 MCU 上跑起来的原因之一。在你准备调用某个算法之前我建议你先打开对应的头文件搜索函数名把函数的注释看一遍。mbedtls 的头文件注释写得非常详细包括参数含义、返回值约定、线程安全问题甚至性能提示都有。很多时候你看一遍头文件比在网上搜半天博客更有效。3. 源码编译的第一步桌面环境快速验证这个阶段的目标只有一个在你的 PC 上把这套源码编译成可用的库并且跑通一个最简单的示例程序。环境就以 Ubuntu 为例Windows 上的 MinGW 或 MSVC 思路类似。3.1 用 CMake 构建和直接编译的选择mbedtls 从 2.x 开始就提供了写得比较顺滑的 CMake 构建方式。在源码根目录里执行tar xzf mbedtls-2.28.8.tar.gz cd mbedtls-2.28.8 mkdir build cd build cmake .. make -j$(nproc)编译完成后library/目录下会生成libmbedtls.a、libmbedx509.a、libmbedcrypto.a三个静态库文件。其中libmbedcrypto.a是纯算法库AES、SHA256、RSA 都在这里面libmbedtls.a是 TLS 协议栈libmbedx509.a是证书相关。如果你只做算法调用链接libmbedcrypto.a一个就够了。如果你是那种不喜欢 CMake 的人也可以直接编译你需要的.c文件。反过来讲我并不建议这样干因为 mbedtls 内部模块之间有依赖关系比如 RSA 依赖 bignumAES-GCM 依赖 AES 和 GHASH你手工挑选源文件很容易漏。老老实实用它自带的构建系统是最稳的。3.2 用programs里的示例验证安装成果编译完不要急着写代码先去programs目录下看看官方示例。比如编译完之后programs/aes/aescrypt2这个可执行文件已经生成好了你可以拿它做一个加解密验证echo hello mbedtls plain.txt ./programs/aes/aescrypt2 0 plain.txt encrypted.enc 0123456789abcdeffedcba9876543210 ./programs/aes/aescrypt2 1 encrypted.enc decrypted.txt 0123456789abcdeffedcba9876543210 cat decrypted.txt0表示加密1表示解密第三个参数是 AES-128 的密钥16 字节写成 32 个十六进制字符。这条链路要是通了说明你手里的源码包是完整的编译工具链没问题AES 模块工作正常后续做二次开发就有了一个可靠的基础。4. 交叉编译把 mbedtls 塞进嵌入式内核源码接下来说嵌入式场景。不管你是用 RTOS 还是裸机本质都是把 mbedtls 源码和你的工程代码编到一起。我以 STM32 GCC 工具链为例讲一下关键点。4.1 如何用交叉编译工具链构建 mbedtls嵌入式构建 mbedtls 有两种主流方式一是用 CMake 指定交叉工具链二是直接把library/下的.c文件添加到你的 IDE 工程里。我个人的经验是如果你的 MCU 工程用的是 Makefile 或 CMake那就用第一种如果用的是 Keil MDK 或 IAR 这类 IDE第二种往往更省事。用 CMake 进行交叉编译时可以这样指定工具链cmake -DCMAKE_C_COMPILERarm-none-eabi-gcc \ -DCMAKE_SYSTEM_NAMEGeneric \ -DCMAKE_SYSTEM_PROCESSORarm \ .. make -j$(nproc)这里关键是CMAKE_SYSTEM_NAMEGeneric它告诉 CMake 这不是一个带操作系统的常规平台避免它去检测 Linux 相关的头文件和库函数。如果你的工程是 Keil MDK做法更直接把library目录下的.c文件全部拖进工程或者只拖你需要的模块然后把include目录加进头文件搜索路径编译就完事了。MDK 不像 CMake 会预编译一堆东西你是直接把这套库的源码合进了你自己的固件工程里。4.2 裁剪配置不加.secret的减法是必须掌握的这是嵌入式移植 mbedtls 的核心经验。默认的mbedtls_config.h开启了几乎所有功能这意味着你会把不必要的算法代码全部编进固件Flash 和 RAM 占用会明显增加。对于只有 64KB RAM 的 MCU 来说这不只是浪费而是不可用。裁剪的基本思路如下先只保留你真正需要的算法比如只需要 AES-CBC 和 SHA256就把 RSA、ECDSA、X509、TLS 协议栈都关掉。然后逐个打开mbedtls_config.h文件把对应的宏注释掉或改为0。配置宏的名字和功能是一一对应的比如MBEDTLS_AES_C控制 AES 源码要不要编译MBEDTLS_SHA256_C控制 SHA256。如果只保留 AES、SHA256、CTR_DRBG 这几个模块配置修改量并不大主要就是注释掉MBEDTLS_RSA_C、MBEDTLS_ECDSA_C、MBEDTLS_SSL_TLS_C等宏。改完之后建议仔细检查一下哪些模块依赖你关掉的模块否则编译的时候报错会让你摸不着头脑。4.3 在 RTOS 环境里跑 mbedtls 的一个隐藏注意点如果你用的是 FreeRTOS 或 RT-Thread需要注意 mbedtls 默认的时间函数mbedtls_time()和底层的calloc/free。在裸机环境里默认配置直接能用但在 RTOS 环境动态内存分配可能会经过你 RTOS 的内存管理模块。这种情况下你最好在mbedtls_config.h里定义MBEDTLS_PLATFORM_MEMORY并通过mbedtls_platform_set_calloc_free()设置你自己的内存分配函数。TLS 握手过程会涉及超时控制、随机数生成等典型的操作是提供一个基于 RTOS tick 的时钟回调函数不然握手超时判断可能不准确。这些细节往往在小型设备上排查问题时才暴露出来但移植之前先了解清楚能躲掉 80% 的玄学报错。5. Python 调用 mbedtls 算法把源码编成动态库来用前面说的是嵌入式源码级集成。还有一个很常见的需求桌面端程序或脚本想用 mbedtls 的高性能算法实现。作者热词中出现了“ppython调用mbedtls算法生成加密的包”这其实就是把 mbedtls 源码编译成动态库再通过 Python 的 ctypes 或者 cffi 来调用从而在 Python 里拿到 C 实现的加密性能同时避免自己用 Python 写一遍不安全的密码学代码。为什么不在 Python 里直接pip install cryptography当然可以。但有时候你手里的加密模块是定制的比如需要使用特定厂商证书体系、特定密钥派生流程或者干脆是为了和嵌入式设备端保持同一个密码库实现避免两边行为不一致。用 ctypes 直接调 mbedtls就能实现“同一套加密算法嵌入式端和 Python 端完全一致”的效果。5.1 第一次编译出.so动态库在桌面 Linux 环境下生成动态库很简单mkdir build-shared cd build-shared cmake -DUSE_SHARED_MBEDTLS_LIBRARYON .. make -j$(nproc)编译完成后library/下会生成libmbedcrypto.so之类的动态库文件。USE_SHARED_MBEDTLS_LIBRARY这个开关是 mbedtls 官方 CMake 体系里现成的不需要你自己改构建脚本。如果你手里只有静态库用gcc -shared -fPIC把静态库再包一层也行但没必要折腾直接编动态库更干净。5.2 用 ctypes 封装 SHA256、AES 接口动态库拿到了接下来就是用 Python 调它。以libmbedcrypto.so为例一个 Python 调 SHA256 的代码如下import ctypes import ctypes.util # 加载动态库 lib_path ctypes.util.find_library(mbedcrypto) if not lib_path: lib_path ./libmbedcrypto.so lib ctypes.CDLL(lib_path) # 定义 SHA256 上下文结构体 class SHA256Context(ctypes.Structure): _fields_ [ (total, ctypes.c_uint32 * 2), (state, ctypes.c_uint32 * 8), (buffer, ctypes.c_ubyte * 64), (is384, ctypes.c_int), ] # 准备函数原型 lib.mbedtls_sha256_init.argtypes [ctypes.POINTER(SHA256Context)] lib.mbedtls_sha256_update.argtypes [ctypes.POINTER(SHA256Context), ctypes.c_void_p, ctypes.c_size_t] lib.mbedtls_sha256_finish.argtypes [ctypes.POINTER(SHA256Context), ctypes.c_char_p] def sha256_hex(data: bytes) - str: ctx SHA256Context() lib.mbedtls_sha256_init(ctypes.byref(ctx)) lib.mbedtls_sha256_update(ctypes.byref(ctx), data, len(data)) digest ctypes.create_string_buffer(32) lib.mbedtls_sha256_finish(ctypes.byref(ctx), digest) return digest.raw.hex() print(sha256_hex(bhello mbedtls))这里有个重要细节mbedtls_sha256_update和finish的缓冲区都是二进制数据在传参时用ctypes.c_char_p可能遇到\x00截断的问题。更稳的做法是把参数类型声明成ctypes.c_void_p然后在 Python 层传入ctypes.c_char_p(data)或者ctypes.create_string_buffer。这块踩坑概率比较高后面统一讲。AES 加解密的调用方式类似核心是初始化mbedtls_aes_context设置密钥然后调用mbedtls_aes_crypt_cbc。需要注意 AES 的上下文结构体定义在不同版本里有差异写 ctypes 结构体时一定要和你当前编译的源码版本一一对应。最简单的办法是在 C 语言那头包一层导出一个“黑盒”函数Python 只管传入明文、密钥收回密文。我后来就是这么干的——写了一层薄薄的 C 封装再用 ctypes 调用省去了反复去比对结构体的烦恼。这其实也就是“生成加密的包”的思路把加密密钥、算法参数封装在一个动态库里对外只暴露encrypt(plaintext, key) - ciphertext。外部拿不到内部结构也替换不了算法实现适合做算法保护或统一密码学栈。6. 移植和调用中的坑实测记录源码编译本身不太难真正的坑分布在两个阶段一个是编译期的宏依赖问题另一个是运行期的内存或参数问题。下面这些全部来自我的实测记录。6.1 编译期报 undefined reference在make的时候如果出现undefined reference to mbedtls_aesXXX通常不是函数缺失而是因为配置MBEDTLS_AES_C被关掉导致library/aes.c没有参与编译。但你在自己代码里调用了相关 API于是链接阶段报错。排查思路很简单# 查看库里有没有对应的符号 nm libmbedcrypto.a | grep mbedtls_aes如果没有输出就是源码没编进去回mbedtls_config.h里检查MBEDTLS_AES_C是否为1。如果符号存在但还是 undefined reference那大概率是链接顺序问题——把-lmbedcrypto放在源文件后面再试一次。6.2 嵌入式编译时内存不足编译没问题但链接的时候提示region FLASH overflowed或region RAM overflowed这种就是裸机工程最常见的问题。原因基本可以锁定为没有裁剪默认配置把 TLS、证书、大数运算全编进去了。有个小技巧先加-ffunction-sections -fdata-sections编译再加-Wl,--gc-sections链接可以让链接器自动丢弃未引用的函数能在不做任何代码修改的情况下省下一截 Flash。但是要提醒你--gc-sections只是“治标”能省掉一部分没用到的函数却不能把你代码里调用了但没必要的模块裁掉。真正的根治方式还是回到mbedtls_config.h去裁剪功能。6.3 MDK/Keil 里编译大量 C 文件时提示函数重复定义这个问题出现在你把library/下的.c全部拖入工程后又手动添加了某些源码文件。mbedtls 有些模块会被拆成多个.c比如ssl_tls.c内部可能依赖ssl_cli.c、ssl_srv.c中的函数会自动找文件而不是让你重复添加。只要避免重复添加文件基本不会出现重复定义。还有一种是配置上开启了MBEDTLS_SELF_TEST这会让每个算法模块里多出一份self_test函数有时会和你自己的测试代码里的函数重名。把MBEDTLS_SELF_TEST关掉就能解决。6.4 Python ctypes 调用时字符串截断这个坑前面提过。用ctypes.c_char_p时必须非常小心因为 C 字符串遇到\x00就终止了。而加密场景下密文和密钥是二进制数据随时可能出现\x00。最可靠的方式是把函数原型全部声明为ctypes.c_void_p然后在 Python 侧使用ctypes.create_string_buffer或者ctypes.cast手动传入指针。一个最小可行封装是这样的lib.mbedtls_sha256_update.argtypes [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] lib.mbedtls_sha256_update.restype ctypes.c_int buf ctypes.create_string_buffer(data, len(data)) ret lib.mbedtls_sha256_update(ctypes.byref(ctx), buf, len(data))其中create_string_buffer的作用是创建一块指定大小的缓冲区并把你的 bytes 数据拷贝进去不会被\x00截断。6.5 随机数生成CTR_DRBG 的种子怎么给很多加密操作都需要随机数。mbedtls 的随机数模块ctr_drbg需要一个种子。在嵌入式环境里这个种子通常来自硬件随机数发生器比如 STM32 的 RNG 外设在 PC 上通常从/dev/urandom读取。Python 调用时更简单直接用os.urandom()生成种子再喂给mbedtls_ctr_drbg_seed()。但要注意种子长度有些版本要求必须满足一定的最小长度默认是 48 字节以实际源码配置为准否则会返回错误码MBEDTLS_ERR_CTR_DRBG_INPUT_TOO_BIG。7. 最后的一些体会回过头看这个mbedtls源码.rar其实它在不同人手里发挥的作用完全不一样。有人拿它去啃 TLS 协议实现有人只想要一套跨平台的加解密算法库还有人直接把它当成“代码字典”需要什么算法就翻对应的源码参考实现。正因为 mbedtls 的代码写得干净、模块化程度高这三种用法都行得通。我自己在项目中反复用它的过程中最深的体会是不要贪多求全。mbedtls 的功能强但真正到你项目里大概率就是那么几个算法——AES、SHA256、RSA 或者 ECDSA。把少数几个算法用透比把整个库一知半解地塞进工程里更有价值。在嵌入式端裁剪掉的每一个宏都是实打实的 Flash 和 RAM在桌面端封装一个好的动态库接口比满目录地交叉引用源码要高效得多。如果你手头的这份源码包版本比较老比如 2.16 或更早建议尽快切到 2.28 LTS 或 3.x 主线。老版本的接口设计和新版差异不小网上能找到的经验贴大多也是基于新版本的。源码这东西版本越新你踩别人踩过的坑的概率就越低。本文还有配套的精品资源点击获取
返回列表