4、CCM认证标签生成:MAC计算过程中的临时缓冲区需求
认证标签,说白了就是CCM模式里用来保证数据完整性的那串“签名”。生成这玩意儿的过程,本质上就是一次CBC-MAC计算。嗯,这里要注意——它跟我们平时理解的纯CBC-MAC还不完全一样,CCM在开头加了一个特殊的B0块,里面塞了Nonce、Associated Data长度、消息长度这些控制信息。
那问题来了:这个MAC计算过程中,到底需要多大的临时缓冲区?
我个人习惯把这个问题拆成三块来看:
- 输入数据的暂存——B0块、附加认证数据、明文消息,这些都得有个地方放
- 中间状态的保存——AES加密的中间结果、当前CBC链值
- 最终标签的生成——加密后的MAC值,以及跟计数器模式加密结果的异或
咱们一个一个说。
4.1 核心缓冲区:CBC-MAC的链值寄存器
CBC-MAC的核心操作其实很简单:每次加密一个16字节的数据块,结果作为下一次加密的“链值”。这个链值寄存器,就是最关键的临时缓冲区。
在CCM的MAC计算中,这个缓冲区只需要16字节。为什么?因为AES的块大小就是16字节,每次加密的输入和输出都是16字节。你想想看,不管你要认证的数据是1KB还是1MB,这个链值寄存器始终只需要16字节。
我在项目中遇到过有人给这个缓冲区分配了64字节甚至128字节,理由是“怕不够用”。其实完全没必要,16字节绰绰有余。
关键点: CBC-MAC链值寄存器 = 16字节,这是硬性需求,不可压缩。
4.2 输入数据的分块缓冲区
CCM的MAC计算是按16字节块处理的。但实际应用中,输入数据不一定是16字节的整数倍。所以我们需要一个分块缓冲区,用来暂存那些“不够一块”的数据。
这个缓冲区的大小,取决于你的实现策略:
| 实现策略 | 缓冲区大小 | 说明 |
|---|---|---|
| 逐字节流式处理 | 16字节 | 每次收满16字节就加密一次,最后处理剩余字节 |
| 整块预加载 | 最大输入块大小 | 一次性加载所有数据,然后按块处理 |
| 零拷贝优化 | 0字节(额外) | 直接引用原始数据缓冲区,但需要对齐处理 |
我个人推荐第一种——流式处理。只需要16字节的分块缓冲区,内存占用最小,而且适合嵌入式场景下的数据分段接收。
小技巧: 如果你用流式处理,记得在最后一块处理时,要处理填充(padding)问题。CCM的CBC-MAC不需要像标准CBC那样做PKCS#7填充,它直接用原始数据长度来控制,但分块缓冲区里的剩余数据必须清零后再加密。
4.3 B0块与附加认证数据的临时构造区
CCM的MAC计算开始前,需要先构造B0块。这个块包含了:
- Flags字节(1字节)——指示Nonce长度、是否有附加认证数据、MAC长度
- Nonce(7-13字节,取决于Flags)
- 消息长度编码(2-8字节,取决于Flags)
B0块固定16字节。你需要在内存中临时构造它,然后送入CBC-MAC链。
同样,附加认证数据(Associated Data)也需要先编码再处理。CCM规定,如果附加认证数据长度小于2^16 - 2^8,就用2字节长度前缀;如果更长,就用6字节前缀。这个编码后的数据,也需要一个临时缓冲区来存放。
我建议的做法是:
/* 临时缓冲区布局示例 */
uint8_t temp_buf[32]; /* 16字节B0块 + 16字节分块缓冲区 */
/* 构造B0块 */
temp_buf[0] = flags; /* 1字节Flags */
memcpy(&temp_buf[1], nonce, nonce_len); /* Nonce */
/* 编码消息长度到末尾 */
encode_length(msg_len, &temp_buf[1 + nonce_len], L);
/* 送入CBC-MAC */
aes_encrypt(&chain_value, temp_buf, &chain_value);
你看,总共只需要32字节的临时缓冲区,就能搞定B0块构造和分块处理。
注意: 千万不要把B0块和分块缓冲区合并成一个16字节的缓冲区!因为B0块送入加密后,你还需要分块缓冲区来处理后续的数据块。如果共用,B0块的数据会被覆盖,导致后续计算错误。我曾经见过有人为了省16字节,把这两个缓冲区重叠,结果调试了三天才发现问题。
4.4 最终标签的生成:额外16字节
MAC计算完成后,你会得到一个16字节的CBC-MAC结果。但CCM的认证标签并不是直接拿这个结果,而是需要跟计数器模式加密的第一个块(Counter 0)做异或。
所以,你还需要:
- 16字节:存放CBC-MAC的最终结果
- 16字节:存放Counter 0的加密结果
- 若干字节:存放最终的认证标签(长度取决于M值,通常是4、6、8、10、12、14、16字节)
不过,这些缓冲区可以复用。我通常的做法是:
- 用链值寄存器(16字节)存放CBC-MAC的最终结果
- 用同一个缓冲区存放Counter 0的加密结果(覆盖链值)
- 异或后,直接截取前M字节作为认证标签
这样,额外只需要16字节就够了。
4.5 总内存占用总结
好了,咱们把所有的临时缓冲区加起来看看:
| 缓冲区用途 | 大小(字节) | 可否复用 |
|---|---|---|
| CBC-MAC链值寄存器 | 16 | 不可复用,全程使用 |
| 分块缓冲区 | 16 | 可复用为B0块构造区 |
| B0块构造区 | 16 | 可与分块缓冲区复用 |
| 附加认证数据编码区 | 6(最大) | 可复用分块缓冲区尾部 |
| 最终标签生成区 | 16 | 可复用链值寄存器 |
| 总计(最小化) | 32 |
你没看错,整个CCM认证标签生成过程,只需要32字节的临时缓冲区。如果实现得巧妙一点,甚至可以把B0块构造和分块处理合并,只需要16字节的链值寄存器 + 16字节的分块缓冲区,总共32字节。
核心结论: CCM认证标签生成的临时缓冲区需求 = 32字节。这是理论最小值,也是我在多个嵌入式项目中的实践经验。
4.6 流程图:MAC计算的内存布局
下面这张图展示了整个MAC计算过程中,临时缓冲区的流转关系:
4.7 避坑指南:我踩过的几个坑
最后,分享几个我在实际项目中遇到的教训:
- 缓冲区对齐问题:有些MCU要求AES加密的输入缓冲区必须4字节对齐。如果你把缓冲区定义成uint8_t数组,然后强制转换成uint32_t指针,可能会触发硬件异常。我建议直接用联合体或者attribute((aligned(4)))来保证对齐。
- 分块缓冲区的清零时机:处理最后一块时,如果数据不足16字节,剩余字节必须清零。我曾经忘了清零,结果MAC值一直不对,排查了两天才发现是缓冲区里残留了上一块的数据。
- B0块构造的顺序:Flags、Nonce、消息长度,这个顺序不能乱。特别是消息长度的编码方式,L值不同,编码长度也不同。我建议写一个专门的编码函数,不要手动拼字节,容易出错。
- 标签截取的长度:CCM支持多种标签长度(4、6、8、10、12、14、16字节)。截取时一定要用memcpy或者直接指针操作,不要用循环逐字节赋值——效率低,而且容易在优化级别高的时候被编译器搞出问题。
嗯,以上就是CCM认证标签生成过程中,临时缓冲区的全部需求。32字节,不多不少。你想想看,一个AES块大小就是16字节,整个MAC计算的核心就是围绕这16字节在转。理解了这一点,内存占用的问题就清晰了。