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字节)

不过,这些缓冲区可以复用。我通常的做法是:

  1. 用链值寄存器(16字节)存放CBC-MAC的最终结果
  2. 用同一个缓冲区存放Counter 0的加密结果(覆盖链值)
  3. 异或后,直接截取前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计算过程中,临时缓冲区的流转关系:

CCM认证标签生成 — 临时缓冲区流转图 输入数据流 分块缓冲区 (16B) B0块构造区 (16B) 复用 链值寄存器 (16B) AES加密引擎 反馈回链值 标签生成区 (16B) 最终结果 Counter 0 加密结果 异或 → 认证标签 说明: 虚线箭头表示缓冲区复用关系。分块缓冲区与B0块构造区可复用同一16字节空间;链值寄存器最终可被标签生成区复用。

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字节在转。理解了这一点,内存占用的问题就清晰了。