尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

嵌入式设备中CMAC算法移植与mbedtls集成实战指南

嵌入式设备中CMAC算法移植与mbedtls集成实战指南 1. 项目缘起为什么要在嵌入式项目中移植CMAC算法最近在做一个物联网网关的项目涉及到与云端进行安全的数据通信。协议栈选用了MQTT over TLS这本身没什么问题用现成的mbedtls库就能搞定。但问题出在业务层的一个特定需求上我们需要对某些关键的控制指令生成一个消息认证码确保指令在传输过程中没有被篡改并且能验证发送方的身份。这个需求听起来很像是HMAC基于哈希的消息认证码的典型场景对吧一开始我也是这么想的。但在和云端对齐协议细节时对方明确要求使用CMAC算法并且指定了AES-128作为底层的分组密码。这就有点意思了。HMAC-SHA256用得好好的为什么要换CMAC深入一聊才明白这其实是行业特定规范的要求。在某些对实时性和资源消耗极其敏感的领域比如工业控制、车联网的某些指令CMAC相比HMAC有几个潜在优势它的计算过程更确定与哈希算法相比在某些硬件上可能有更优的加速支持并且其输出长度固定为分组密码的分组大小如AES是16字节比HMAC-SHA256的32字节更短对于带宽受限的无线传输来说能节省一点是一点。然而当我打开手头这个基于STM32的工程检查已有的mbedtls配置时心里凉了半截。mbedtls_config.h里压根没开启MBEDTLS_CMAC_C这个宏。这意味着尽管mbedtls作为一个功能丰富的密码库包含了CMAC的实现但在默认的裁剪配置或我们之前为了节省Flash空间而做的精简配置中它被禁用了。所以“移植”这个词在这里的准确含义其实是在我们的特定工程环境中正确地启用、配置并集成mbedtls的CMAC模块使其能够无缝地为我们所用并确保其运行稳定、内存占用可控。这个过程远不止是打开一个编译开关那么简单涉及到依赖关系梳理、内存管理考量、API的正确使用以及性能测试接下来我就把这趟“移植”之旅的完整过程和踩过的坑分享出来。2. CMAC算法核心原理与mbedtls实现窥探在动手修改代码之前我觉得有必要先搞明白CMAC到底是怎么工作的以及mbedtls是如何实现它的。这能帮助我们在后续配置和调试时做到心中有数而不是盲目地照搬。CMAC全称Cipher-based Message Authentication Code它是一种基于对称分组密码如AES、DES来构造消息认证码的方案。你可以把它理解为一个“带密钥的校验和”但比简单的CRC或MD5要安全得多因为不知道密钥的人无法伪造有效的MAC。它的核心思想来自于早期的CBC-MAC但解决了CBC-MAC在处理可变长度消息时的安全缺陷。CMAC算法的计算过程可以概括为以下几个关键步骤我们以最常用的AES-128-CMAC为例即分组大小128位16字节子密钥生成首先算法会根据输入的用户密钥K推导出两个子密钥K1和K2。这个推导过程涉及对全零数据块进行AES加密然后根据加密结果进行比特移位和与常数的异或操作。这两个子密钥是预计算的只要密钥K不变它们就可以被重复使用是CMAC算法的“调味料”。消息分组与填充将待认证的消息M按16字节分组。如果最后一个分组是完整的16字节则在其末尾异或子密钥K1如果最后一个分组不足16字节则先进行特定的填充填充一个比特‘1’和若干比特‘0’然后再异或子密钥K2。这一步确保了无论消息长度如何最终处理的结构都是安全的。CBC-MAC核心计算用一个初始向量IV通常为全零开始将处理后的消息分组依次进行AES加密并将前一个密文分组与当前明文分组异或后再加密即CBC模式。最后一个分组的加密输出取最左边的若干字节通常为整个分组或指定长度就得到了最终的CMAC值。在mbedtls中这些步骤被优雅地封装了起来。我们最需要打交道的结构体是mbedtls_cmac_context_t。这个结构体内部通常会包含一个底层密码的上下文比如mbedtls_cipher_context_t用于执行AES加密。存储生成的两个子密钥K1和K2。可能还有内部缓冲区用于处理未完成的分组数据。对应的核心API包括mbedtls_cmac_init(): 初始化上下文。mbedtls_cmac_setkey(): 设置密钥这个函数内部会完成上述的子密钥生成步骤。mbedtls_cmac_update(): 输入消息数据可以多次调用。mbedtls_cmac_finish(): 结束计算输出最终的CMAC值。mbedtls_cmac_free(): 释放上下文资源。理解了这个流程我们就能意识到启用CMAC功能不仅仅是要MBEDTLS_CMAC_C它还强依赖底层的分组密码模块例如MBEDTLS_AES_C。如果我们的工程里连AES都没启用那CMAC就是无源之水。3. 工程配置与依赖关系梳理开启正确的“功能开关”我的工程使用的是STM32CubeIDEmbedtls以源码形式放在Middlewares/Third_Party/mbedtls目录下。移植的第一步就是修改配置文件。通常我们会有一个项目级的mbedtls_config.h它可能复制自mbedtls/include/mbedtls/config.h并在此基础上进行裁剪。注意千万不要直接修改mbedtls原生的config.h文件而应该使用项目中的副本或自定义配置头文件并通过编译器-I选项指定其路径。这是保持库源码纯净、便于升级的好习惯。首先找到并确保以下核心宏被定义取消注释或设置为1#define MBEDTLS_CMAC_C紧接着检查其依赖项。根据mbedtls源码中的check_config.h文件这是一个用于验证配置依赖的脚本虽然不参与编译但极具参考价值MBEDTLS_CMAC_C依赖于#define MBEDTLS_AES_C // 或者其他的分组密码如 MBEDTLS_DES_C, MBEDTLS_CAMELLIA_C 等取决于你用哪种算法做CMAC。因为我们用的是AES-128所以必须启用MBEDTLS_AES_C。顺藤摸瓜AES可能又依赖于平台相关的熵源或随机数生成器如果使用了一些特定模式但在最基本的ECB/CBC/CMAC使用中通常只需要AES的软件实现。为了保险起见我建议同时检查#define MBEDTLS_CIPHER_C // 密码算法通用接口层CMAC通过它来调用底层AES。 #define MBEDTLS_MD_C // 消息摘要通用接口层虽然CMAC不直接属于MD但某些配置或示例可能有关联。实际上在mbedtls的模块化设计中CMAC模块直接通过CIPHER模块来操作AES。所以MBEDTLS_CIPHER_C通常是必须的。配置完成后编译一下试试。果然我遇到了第一个错误undefined reference tombedtls_cipher_setkey。这说明CIPHER模块确实被CMAC调用了但我们的配置可能还没完全传递到编译链。仔细检查发现MBEDTLS_CIPHER_C已经打开了但问题可能出在链接阶段。确保你的编译单元.c文件正确包含了mbedtls/cmac.h头文件并且在链接时mbedtls库的相关源文件如cmac.c,aes.c,cipher.c,cipher_wrap.c都被正确编译并链接进了最终的可执行文件。在CubeIDE或Makefile中你需要确认Middlewares/Third_Party/mbedtls/library目录下的这些.c文件是否被添加到项目的源文件列表中。这是一个常见的坑只修改了头文件配置却忘了将对应的源文件加入编译。4. 内存与资源考量在有限的Flash和RAM中做选择对于STM32F4这类资源有限的MCU每增加一个功能模块都需要权衡。启用CMAC和AES会带来多大的空间开销我做了个简单的对比测试。在mbedtls_config.h中我分别注释和取消注释相关宏进行编译观察生成的.map文件或IDE输出的尺寸信息基线配置仅启用TLS客户端所需的最基本模块如RSA、SHA256、随机数生成器等不包含AES和CMAC。Flash占用约80KB。启用AES打开MBEDTLS_AES_C和MBEDTLS_CIPHER_C。Flash占用增加了约8-10KB。这主要是AES的查表法实现S盒等和CIPHER通用框架的代码。启用CMAC在AES基础上再打开MBEDTLS_CMAC_C。Flash占用额外增加约2-3KB。这个增量相对较小因为CMAC的逻辑本身不复杂大部分工作由底层的AES完成。RAM方面主要关注运行时堆栈和动态内存。mbedtls_cmac_context_t结构体本身的大小可以通过sizeof()查看在我的平台上大约是100多字节。更重要的是AES加密操作本身可能需要内部缓冲区。如果使用mbedtls提供的mbedtls_cipher_context_t它内部会根据所选算法和模式分配内存。提示为了精确控制内存尤其是在没有操作系统或使用静态内存池的嵌入式系统中建议在初始化时明确指定内存分配函数。mbedtls允许通过mbedtls_platform_set_calloc_free()来自定义。但更常见的做法是直接使用库默认的在禁用MBEDTLS_PLATFORM_MEMORY时就是标准库的calloc和free并确保你的堆空间足够。一个mbedtls_cmac_context_t加上一次AES操作的开销通常不会超过1KB的临时RAM消耗。如果你的资源极其紧张还可以考虑是否启用AES的硬件加速。STM32F4系列具有Crypto硬件加速器可以大幅提升AES运算速度并可能降低CPU负载。但这需要启用MBEDTLS_AES_ALT并实现相应的硬件驱动层接口这属于更深度的移植本次项目由于时间关系我暂时采用了软件实现后续可以作为一个优化点。5. 实战集成从API调用到功能验证配置和编译通过后就到了实际的代码集成环节。我们的目标是在业务代码中对一段给定的消息和密钥生成其CMAC。首先包含必要的头文件#include mbedtls/cmac.h #include mbedtls/cipher.h // 可选用于更底层的操作或错误码 #include string.h // for memcpy, memset然后编写一个生成CMAC的示例函数/** * brief 使用AES-128-CMAC生成消息认证码 * param key: 16字节的AES密钥 * param message: 待认证的消息 * param msg_len: 消息长度 * param mac: 输出缓冲区至少16字节用于存放生成的CMAC * return 0成功其他为mbedtls错误码 */ int generate_aes128_cmac(const unsigned char *key, const unsigned char *message, size_t msg_len, unsigned char *mac) { int ret 0; mbedtls_cmac_context_t ctx; size_t mac_len 16; // AES-128-CMAC输出16字节 // 1. 初始化上下文 mbedtls_cmac_init(ctx); // 2. 设置密钥指定使用AES-128算法 ret mbedtls_cmac_setkey(ctx, MBEDTLS_CIPHER_AES_128_ECB, key, 128); if (ret ! 0) { printf(mbedtls_cmac_setkey failed! ret -0x%04X\n, -ret); goto cleanup; } // 3. 输入消息数据 ret mbedtls_cmac_update(ctx, message, msg_len); if (ret ! 0) { printf(mbedtls_cmac_update failed! ret -0x%04X\n, -ret); goto cleanup; } // 4. 结束计算获取MAC ret mbedtls_cmac_finish(ctx, mac, mac_len); if (ret ! 0) { printf(mbedtls_cmac_finish failed! ret -0x%04X\n, -ret); goto cleanup; } // 理论上mac_len应该等于16可以加个断言检查 // MBEDTLS_ASSERT(mac_len 16); cleanup: // 5. 释放资源 mbedtls_cmac_free(ctx); return ret; }这段代码看起来很简单但有几个细节需要注意算法标识mbedtls_cmac_setkey的第二个参数是cipher_type。这里我们用了MBEDTLS_CIPHER_AES_128_ECB。注意虽然CMAC内部使用的是CBC模式的思想但设置密钥时指定的是底层的密码算法和密钥长度模式参数ECB在这里可能被忽略或仅作为标识。查阅mbedtls源码确认对于CMAC它主要关心的是密码类型和密钥长度。使用MBEDTLS_CIPHER_AES_128_ECB是正确的。密钥长度第三个参数keybits是128对应AES-128。务必确保传入的key指针指向的数据确实是16字节。错误处理mbedtls的函数通常返回0表示成功负数表示错误。错误码通常是一个负的十六进制数。通过-ret可以将其转换为正数便于打印。在生产代码中应该有更健壮的错误处理逻辑。资源清理无论成功与否都必须调用mbedtls_cmac_free来释放上下文内部可能申请的资源防止内存泄漏。这是良好的编程习惯。接下来我们需要验证这个函数是否正确工作。我采用了“已知答案测试”的方法。从NIST的官方测试向量可以在网上搜索“NIST CMAC test vectors”中找一组AES-128-CMAC的测试数据。例如我使用了以下测试向量密钥K:2b7e1516 28aed2a6 abf71588 09cf4f3c消息M: (空字符串)预期CMAC:bb1d6929 e9593728 7fa37d12 9b756746编写一个测试函数void test_cmac_basic(void) { unsigned char key[16] {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; unsigned char message[1] {0}; // 空消息 unsigned char expected_mac[16] {0xbb, 0x1d, 0x69, 0x29, 0xe9, 0x59, 0x37, 0x28, 0x7f, 0xa3, 0x7d, 0x12, 0x9b, 0x75, 0x67, 0x46}; unsigned char calculated_mac[16] {0}; int ret generate_aes128_cmac(key, message, 0, calculated_mac); // 长度为0的空消息 if (ret 0) { if (memcmp(calculated_mac, expected_mac, 16) 0) { printf(CMAC test passed!\n); } else { printf(CMAC test failed! Output mismatch.\n); // 可以在这里打印出计算得到的mac进行对比 } } else { printf(CMAC calculation failed with error: %d\n, ret); } }将测试代码加入工程在MCU上运行通过串口查看输出。如果看到“CMAC test passed!”那么恭喜你最基本的CMAC功能已经移植成功了。我强烈建议多找几组测试向量包括不同长度的消息进行验证确保边界条件如刚好一个分组、超过一个分组等下也能正确工作。6. 踩坑与优化实际应用中的经验之谈在将CMAC集成到真实的数据上报和指令下发流程中时我遇到了几个预料之外的问题这里分享出来希望能帮你避坑。第一个坑多线程/中断环境下的上下文复用。我的应用场景中可能有多个任务或中断服务程序需要计算不同消息的CMAC。最初我为了节省每次初始化和释放的开销尝试定义一个全局的mbedtls_cmac_context_t变量在不同地方重复使用。结果出现了计算错误或者偶尔的异常。原因分析mbedtls_cmac_context_t结构体内部是有状态的。在调用mbedtls_cmac_finish()之后其内部状态已经改变可能包含了最终的MAC值或中间数据。如果此时不经过mbedtls_cmac_free()和mbedtls_cmac_init()重置直接用它为新的消息调用mbedtls_cmac_update()会导致计算基于错误的状态进行。更危险的是如果在计算过程中即update之后finish之前被高优先级中断打断而中断服务程序也使用了同一个全局上下文那么状态会被彻底破坏。解决方案为每个独立的CMAC计算会话使用独立的上下文。如果计算不频繁最简单可靠的做法就是在栈上局部声明上下文像上面的示例代码一样让函数管理其生命周期。如果出于性能考虑必须复用则必须严格序列化访问。可以使用互斥锁在RTOS中或关中断在裸机中来保护整个CMAC计算序列从init或setkey到finish。并且在每次计算序列完成后调用mbedtls_cmac_free()然后在下一次计算前重新init和setkey。但这样做的开销可能并不比局部变量小多少还增加了复杂性。实测下来对于我们的应用频率局部变量的方式完全可接受。第二个坑密钥管理的安全性与性能。我们的项目密钥是预先烧录在Flash中的。每次计算CMAC都需要调用mbedtls_cmac_setkey这个函数内部会执行AES加密来生成子密钥K1和K2。这是一个相对耗时的操作相比于update和finish。优化思路如果同一个密钥需要用来计算大量消息的CMAC例如用同一个设备密钥认证所有上行消息那么反复计算子密钥就是一种浪费。我们可以自己缓存子密钥吗理论上可以但需要理解mbedtls内部结构不推荐。一个更优雅的利用mbedtls自身机制的优化方法是将上下文初始化、设置密钥的过程提前在系统初始化时完成一次然后将准备好的上下文保存起来备用。但如前所述上下文不能直接用于并发计算。我的折中方案是创建一个“上下文模板”。在系统启动时用设备密钥初始化一个CMAC上下文并调用mbedtls_cmac_setkey。然后深拷贝这个已设置好密钥的上下文。mbedtls没有提供直接的深拷贝函数但我们可以通过观察其结构体定义在cmac.h中发现它主要包含一个cipher_context_t和两个子密钥数组。我们可以自己实现一个拷贝函数或者更简单地将setkey所需的参数算法类型、密钥缓存起来在每次需要时快速创建新的上下文并setkey。虽然setkey仍有开销但避免了每次都从Flash读取和解析密钥的额外操作如果密钥存储格式复杂的话。实际上对于STM32F4一次AES-128setkey的软件计算开销在微秒级对于大多数物联网应用秒级或分钟级的消息间隔来说这根本不是瓶颈。因此我最终选择了最简单的“每次用时创建”模式代码清晰安全性也好密钥材料在栈上停留时间短。第三个坑输出MAC的长度与协议对齐。我们的云端协议规定CMAC输出取前8字节64位作为认证码。而mbedtls_cmac_finish默认输出整个分组长度16字节。这需要我们在调用finish后手动截取前8字节。unsigned char full_mac[16]; size_t mac_len 16; ret mbedtls_cmac_finish(ctx, full_mac, mac_len); if (ret 0) { memcpy(protocol_mac, full_mac, 8); // 协议规定的8字节MAC }务必和你的协议方确认好MAC长度。CMAC算法本身支持输出小于等于分组长度的任意比特长度但mbedtls的finish函数似乎只输出完整字节且是分组大小。如果需要特定位数如40位可能需要对输出字节进行掩码操作。7. 进阶话题与TLS共存的配置与调试技巧我们的工程中已经使用了mbedtls的TLS部分。现在又加入了CMAC模块这可能会引入一些微妙的交互或配置冲突。配置一致性检查确保TLS和CMAC使用的密码套件没有底层冲突。例如TLS如果使用了AES-256-GCM那么MBEDTLS_AES_C肯定已经启用并且可能还启用了MBEDTLS_GCM_C。我们的CMAC使用AES-128-ECB这两者可以共存。但要注意如果TLS配置为了极致精简只使用了CHACHA20-POLY1305这种流密码而没有启用AES那么你单独为CMAC启用AES就会增加额外的代码体积。需要评估是否值得。内存池冲突有些深度定制的mbedtls移植可能会使用静态内存池来替代动态内存分配。如果TLS和CMAC模块共享同一个内存池需要确保池的大小足够容纳两者同时操作所需的内存。在我们的项目中由于没有启用静态内存池MBEDTLS_MEMORY_BUFFER_ALLOC_C未定义所以不存在这个问题使用的是系统堆。调试技巧当CMAC计算出现问题时除了使用测试向量还可以利用mbedtls的调试功能。在mbedtls_config.h中启用MBEDTLS_DEBUG_C并在代码中调用mbedtls_debug_set_threshold(4)或更高等级。然后在mbedtls_cmac_setkey、update、finish等函数调用前后虽然CMAC模块本身可能没有太多调试输出但底层的AES或CIPHER模块可能会打印出有价值的信息帮助你判断是密钥设置错误还是数据块处理问题。另一种更直接的调试方法是“白盒对比”。在PC上使用OpenSSL命令行工具计算相同密钥和消息的CMAC与嵌入式端的结果进行比对。OpenSSL命令示例# 假设密钥和消息是十六进制字符串 echo -n 这里是你的消息 | openssl aes-128-cbc -K echo -n 你的16字节密钥十六进制字符串 | xxd -p -iv 00000000000000000000000000000000 -nopad | tail -c 16 | xxd -p注意OpenSSL的enc -aes-128-cbc命令在-nopad模式下配合全零IV其最后分组的输出与CMAC算法在概念上有相似之处但严格来说并不直接等价于CMAC。最可靠的方法还是用Python的cryptography库或在线CMAC计算工具生成标准测试向量进行比对。8. 总结与最终集成效果经过上述步骤我们成功地在基于STM32和mbedtls的嵌入式项目中“移植”并集成了AES-128-CMAC功能。回顾整个过程关键点在于明确需求确认必须使用CMAC而非HMAC。理解依赖知道CMAC依赖于CIPHER和AES或其他分组密码模块。正确配置在mbedtls_config.h中精准地开启MBEDTLS_CMAC_C及其所有依赖宏并确保对应源文件参与编译。资源评估了解增加的Flash/RAM开销并在项目约束内做出选择。API熟练使用掌握init、setkey、update、finish、free这一套标准流程并注意错误处理和上下文生命周期管理。严格验证使用NIST等权威测试向量进行验证确保算法实现的正确性。应对实际场景处理好并发、密钥管理、MAC长度裁剪等工程细节。最终我将CMAC生成函数封装成一个独立的模块提供了清晰的接口。在数据上报时对报文关键字段进行CMAC计算并将8字节的MAC附加在帧尾在接收云端指令时先校验CMAC通过后才执行逻辑。整个机制上线后运行稳定满足了项目的安全通信需求。移植过程中最深的体会是在嵌入式环境下使用密码学库三分靠理解算法七分靠工程配置和细节把控。mbedtls的模块化设计给了我们很大的灵活性但也要求我们对模块间的依赖关系有清晰的认知。希望这篇详细的记录能帮助你在遇到类似需求时少走一些弯路。
返回列表