ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

嵌入式设备安全升级:从mbedtls库中移植AES-CMAC算法实战

2026/8/6 9:34:20 拓冰建站 浏览量
嵌入式设备安全升级:从mbedtls库中移植AES-CMAC算法实战 1. 项目概述为什么要在嵌入式里折腾CMAC最近在做一个物联网终端设备的安全升级项目客户要求对固件升级包进行强完整性校验和来源认证。方案评审时大家一致否决了简单的MD5或SHA-1认为在当前的攻击环境下强度不够。最终我们决定采用基于AES的CMAC算法。原因很简单它算是消息认证码里的“实力派”基于成熟的分组密码安全性有保障而且计算资源消耗相对可控非常适合我们手头那些RAM和Flash都捉襟见肘的MCU。但问题来了我们主控芯片的SDK里并没有现成的CMAC实现。自己从头实现一个密码学这东西自己写容易出鬼一个细微的时序或逻辑错误就可能导致严重的安全漏洞。于是我们把目光投向了mbedtls这个在嵌入式领域声名显赫的密码学库。它模块化设计清晰代码质量高且拥有宽松的Apache 2.0许可证。我们的任务很明确从mbedtls这个“大工具箱”里把CMAC这个“精密工具”单独拆出来移植到我们的裸机环境里并且要确保它跑得稳、占得少。这不仅仅是调用几个API那么简单。你需要理解CMAC在mbedtls里是怎么被“组装”起来的它依赖哪些底层模块如何裁剪掉我们用不上的部分以及如何适配到没有操作系统、没有标准库的环境。这个过程充满了对密码学原理、代码结构和嵌入式系统理解的考验。接下来我就把这次移植mbedtls的CMAC算法的核心思路、实操步骤和踩过的坑毫无保留地分享给你。2. 核心思路与模块依赖分析2.1 CMAC算法原理与mbedtls实现窥探在动手移植前必须搞清楚我们要搬的“家具”内部结构。CMAC全称Cipher-based Message Authentication Code其核心思想是利用分组密码如AES来生成一个固定长度的标签用于验证消息的完整性和真实性。mbedtls中的CMAC实现位于library/cmac.c。阅读其源码你会发现它并不是一个完全独立的黑盒。它的工作流程大致是子密钥生成根据核心密钥K通过加密一个全零数据块并经过一系列移位和异或操作生成两个子密钥K1和K2。这是CMAC算法的关键步骤。消息分组处理将输入消息按分组长度AES是128位/16字节分块。对前面的所有完整块使用核心密钥进行加密。最后块特殊处理对最后一个块根据其是否是完整块选择与K1或K2进行异或后再进行加密。输出标签取最后一次加密结果的左半部分例如对于128位AES-CMAC取前16字节作为最终的MAC值。在mbedtls中这个流程被封装成几个关键函数mbedtls_cmac_setkey,mbedtls_cmac_update,mbedtls_cmac_finish。但更重要的是这些函数高度依赖另一个模块mbedtls_cipher。CMAC本身不直接操作AES的加密解密它通过一个统一的cipher接口来调用具体的分组密码算法。这就意味着要CMAC能跑你必须至少准备好Cipher模块以及一个具体的Cipher实现比如AES。2.2 关键依赖模块拆解因此我们的移植清单变得清晰起来这是一个自上而下的依赖链核心目标mbedtls/cmac.c和include/mbedtls/cmac.h直接依赖mbedtls/cipher.c提供统一的密码算法操作接口。mbedtls/cipher_wrap.c这个文件包含了具体算法如AES对cipher接口的“包装”实现是连接抽象接口和具体实现的桥梁。include/mbedtls/cipher.h算法实现依赖mbedtls/aes.c我们需要的AES具体实现。这是计算的主力。include/mbedtls/aes.h平台适配依赖mbedtls/platform.c或mbedtls/platform_util.c提供内存操作、随机数生成等平台相关函数的替代实现。在裸机环境我们通常需要自己实现或裁剪它。include/mbedtls/platform.h或include/mbedtls/platform_util.h基础工具依赖mbedtls/bignum.c等等先别急对于单纯的AES-CMAC实际上并不依赖大数库。这是一个常见的误区。仔细查看源码CMAC和底层AES的运算都在字节和字级别完成不涉及大整数运算。这能为我们节省大量代码空间。注意这种模块化依赖分析是移植成功的第一步。盲目地把整个mbedtls库都搬过来会让你的固件体积爆炸。一定要根据功能需求按需抽取。2.3 头文件与配置的迷宫mbedtls通过一个名为mbedtls/config.h的配置文件来启用或禁用几乎所有功能。默认的config.h内容繁多。我们需要创建一个极简的版本。你需要在这个配置文件中明确#define MBEDTLS_CMAC_C启用CMAC模块。#define MBEDTLS_CIPHER_C启用Cipher抽象层。#define MBEDTLS_AES_C启用AES算法。很可能需要#define MBEDTLS_CIPHER_MODE_ECB因为CMAC内部使用AES的ECB模式进行加密运算。关闭所有其他无关的宏例如MBEDTLS_BIGNUM_C,MBEDTLS_SHA256_C,MBEDTLS_ENTROPY_C等。头文件的包含路径也要处理好。你需要让编译器能找到mbedtls/目录下的头文件并且你的config.h必须在包含路径中或者直接放在mbedtls/目录下替换原文件。3. 移植实操从源码到可执行代码3.1 源码文件抽取与工程集成首先在你的嵌入式项目目录下创建一个子目录比如third_party/mbedtls/。将分析好的必需源码文件复制进来third_party/mbedtls/ ├── include/ │ ├── mbedtls/ │ │ ├── aes.h │ │ ├── cmac.h │ │ ├── cipher.h │ │ ├── platform_util.h │ │ └── ... (其他必要的头文件) │ └── 你的 config.h 也可以放在这里或单独管理 └── library/ ├── aes.c ├── cipher.c ├── cipher_wrap.c (尤其是 aes.c 对应的包装器如 cipher_wrap.c 中与AES相关的部分) ├── cmac.c └── platform_util.c这里有一个关键细节cipher_wrap.c文件通常包含了所有算法的包装函数。你需要仔细查看只提取与AES相关的静态函数如static int aes_crypt_ecb_wrap和相关的mbedtls_cipher_info_t结构体定义而不是整个文件。更好的方法是参考该文件自己为AES实现一个精简的“包装器”只暴露CMAC所需的接口。然后在你的IDE或Makefile中将这些.c文件添加到编译源文件列表并将include目录添加到头文件搜索路径。3.2 平台适配层实现这是移植中最容易出问题的一环。mbedtls库内部会调用一些标准库函数如memset,memcpy,memcmp。在裸机环境下你需要提供这些函数的实现。通常编译器自带的运行时库如newlib-nano会提供它们但有时为了更严格控制我们会自己实现或使用芯片厂商提供的轻量级实现。更重要的是mbedtls/platform_util.c中的函数。这个文件里有两个关键函数mbedtls_platform_zeroize()用于安全清零内存防止敏感信息如密钥残留在内存中。在资源受限且没有MMU的MCU上一个简单的memset实现可能因为编译器优化而被忽略。安全的实现可能需要使用volatile指针。这里给出一个参考实现void mbedtls_platform_zeroize( void *buf, size_t len ) { volatile unsigned char *p (volatile unsigned char *)buf; while( len-- ) { *p 0; } }mbedtls_calloc()和mbedtls_free()CMAC模块内部默认使用动态内存分配来创建上下文结构体。在嵌入式系统中动态内存分配是很多不稳定问题的根源尤其是内存碎片。强烈建议修改配置或源码改为静态分配。修改方法在config.h中定义#define MBEDTLS_CMAC_ALT然后你自己实现一组mbedtls_cmac_xxx函数在内部使用静态全局变量或由调用者传入的静态缓冲区。或者更直接地修改cmac.c将mbedtls_cipher_context_t的分配改为使用静态数组。这需要你深入理解上下文结构并小心处理多任务重入问题如果存在RTOS。3.3 配置裁剪与编译优化你的config.h文件是控制代码大小的总开关。除了开启必要的模块还要关闭所有调试和冗余功能#define MBEDTLS_CMAC_C #define MBEDTLS_CIPHER_C #define MBEDTLS_AES_C #define MBEDTLS_CIPHER_MODE_ECB // 关闭大量不必要功能 #undef MBEDTLS_SELF_TEST // 非常重要关闭自测代码能省下大量空间 #undef MBEDTLS_ERROR_C // 关闭详细的错误字符串只保留错误码 #undef MBEDTLS_VERSION_C #undef MBEDTLS_PLATFORM_TIME_ALT // ... 关闭其他所有你确认不需要的宏编译时开启最高级别的优化如-Os优化尺寸-O2/-O3优化速度并确保消除未使用的函数和符号GCC的-ffunction-sections -fdata-sections配合链接器的--gc-sections。3.4 编写测试向量验证移植完成后绝对不能假设它是正确的。必须使用标准测试向量进行验证。NIST官方提供了AES-CMAC的测试向量。你可以写一个简单的测试函数放在主循环或通过调试接口调用。#include “mbedtls/cmac.h” #include “mbedtls/aes.h” #include stdio.h // 或你的串口打印函数 int test_cmac(void) { mbedtls_cipher_context_t ctx; unsigned char key[16] { ... }; // 测试密钥 unsigned char msg[] { ... }; // 测试消息 unsigned char mac[16]; // 存放结果 const unsigned char expected_mac[16] { ... }; // 预期结果 mbedtls_cipher_init(ctx); // 选择AES-128-ECB算法CMAC内部使用 mbedtls_cipher_setup(ctx, mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB)); mbedtls_cipher_cmac_starts(ctx, key, 128); // 128位密钥 mbedtls_cipher_cmac_update(ctx, msg, sizeof(msg)); mbedtls_cipher_cmac_finish(ctx, mac); mbedtls_cipher_free(ctx); if(memcmp(mac, expected_mac, 16) 0) { my_printf(“CMAC test PASSED!\n”); return 0; } else { my_printf(“CMAC test FAILED!\n”); // 打印出计算得到的mac和预期的mac进行比对 return -1; } }通过所有测试向量才能证明你的移植在功能上是正确的。4. 性能优化与资源管理实战4.1 内存占用深度剖析移植完成后使用编译器的map文件分析内存占用是必修课。你会主要关注两部分Flash代码占用由你编译进来的.c文件决定。通过裁剪config.h和移除无用函数通常能将AES-CMAC相关的代码控制在10-20KB左右这对于现代Cortex-M系列MCU来说是可以接受的。RAM数据占用这是更关键的资源。主要占用来自上下文结构体mbedtls_cipher_context_t和内部CMAC状态。查看结构体定义估算其大小。AES-128的上下文大概在200字节左右。栈空间加解密运算过程中的局部变量和函数调用栈。确保你的任务栈或主栈有足够空间通常预留1-2KB给密码运算比较安全。动态内存如前所述务必消除动态内存分配。一个实用的技巧是将CMAC上下文定义为全局静态变量或者作为模块内部静态变量。如果系统中有多个地方需要用到CMAC但非并发可以考虑设计一个带互斥锁的上下文复用机制以节省RAM。4.2 计算速度优化策略在资源受限的MCU上AES运算速度可能是瓶颈。优化可以从几个层面考虑硬件加速这是最有效的途径。许多现代MCU如STM32的CRYP外设NXP的CAUESP32的AES加速器都内置了硬件AES引擎。mbedtls的AES模块通常通过MBEDTLS_AES_ALT宏来支持硬件加速实现。你需要查阅芯片手册实现一组替代函数如mbedtls_aes_encrypt_alt,mbedtls_aes_decrypt_alt并在config.h中启用MBEDTLS_AES_ALT。这可以将AES运算速度提升数十甚至上百倍。查表法优化如果无硬件加速mbedtls的默认AES实现使用了查表法T-table这是一种以空间换时间的策略。它需要约4KB的查找表只读可放在Flash。确保你的config.h中启用了MBEDTLS_AES_ROM_TABLES如果表是常量以获得最佳性能。禁用此选项会使用动态计算速度慢但代码小。减少数据搬运在mbedtls_cmac_update中如果可能尽量让输入数据直接来自设备接口如SPI Flash避免先读到一个大缓冲区再计算可以边读边计算节省RAM和时间。4.3 安全注意事项密钥安全CMAC的强度完全依赖于密钥。绝对不要将硬编码的密钥明文存放在Flash中。应使用芯片的唯一IDUID结合安全启动流程派生密钥或使用安全元件SE存储。时序攻击软件实现的密码算法可能受到时序攻击的威胁。虽然CMAC本身结构相对规整但底层的AES实现和内存比较函数如验证MAC时的memcmp需要是常数时间的。mbedtls提供了一些常数时间函数如mbedtls_ct_memcmp在验证MAC值时应该使用它们。清零上下文运算结束后务必调用mbedtls_cipher_free()或你自己的清理函数它会调用platform_zeroize来清理上下文中的密钥和中间状态。5. 常见问题排查与调试心得5.1 链接错误与未定义符号这是移植初期最常见的问题。通常是因为依赖关系没理清或者配置文件config.h的宏定义不匹配。症状链接器报错提示undefined reference tombedtls_cipher_info_from_type 等。排查检查对应的.c文件这里是cipher.c是否已加入编译。检查config.h中是否定义了对应的宏这里是MBEDTLS_CIPHER_C。使用编译命令gcc -E ...对出问题的源文件进行预处理查看宏展开后相关函数定义是否还存在。5.2 计算结果不正确症状测试向量通不过计算出的MAC值与预期不符。排查步骤确认密钥和消息输入用十六进制打印函数确保你传递给CMAC函数的密钥和消息字节序列完全正确没有大小端问题。嵌入式系统中从网络或存储设备读取的数据字节序要特别注意。单步调试AES基本运算写一个最简单的AES-ECB加密测试使用一个已知的密钥和明文看加密结果是否正确。如果这里就错了那么问题出在AES层。跟踪CMAC子密钥生成在cmac.c的cmac_generate_subkeys函数内部设置断点或打印日志查看生成的K1和K2是否正确。这是CMAC算法最容易出错的地方之一。检查分组处理逻辑特别是最后一块数据的处理判断“是否是完整块”的逻辑是否正确。5.3 内存越界与系统崩溃症状程序运行CMAC计算后死机、重启或数据异常。排查栈溢出这是最大的嫌疑。增大当前任务的栈空间或者将CMAC计算函数放到栈更充裕的任务中执行。上下文结构体大小确认你分配的mbedtls_cipher_context_t缓冲区大小足够。可以查看cipher.h中该结构体的定义或者直接使用sizeof()获取。动态内存分配如果你没有成功禁用动态分配检查你的堆heap大小是否足够。在启动文件或链接脚本中调整堆的大小。5.4 性能不达标症状计算一个MAC耗时过长影响系统实时性。排查与优化使用硬件加速这是首要检查项。编译器优化等级确认编译时开启了-O2或-Os。查找表位置确认AES的T表被链接到了访问速度快的Flash区域如ITCM或带缓存的区域而不是慢速Flash。剖析代码使用MCU的周期计数器如DWT-CYCCNT对mbedtls_cipher_cmac_finish函数进行打点确定耗时主要在哪一步。移植mbedtls的CMAC就像在嵌入式设备上搭建一个微型的安全堡垒。它不需要豪华的配置但要求每一块砖都砌得扎实可靠。这个过程让我深刻体会到在资源受限的环境下实现安全功能平衡性能、体积和安全性是一门艺术。最终当你的设备能够快速、稳定地输出正确的CMAC值时那种成就感是对所有调试和优化工作的最好回报。