2. CCM核心参数

好,咱们直接进入正题。AES-CCM 这个模式,说白了就是 CTR 模式加密 + CBC-MAC 认证的组合体。你想想看,一个算法要同时干两件事,参数自然就多。我见过不少新手,上来就调库,结果 Nonce 长度搞错,Tag 截断太短,最后上线被攻击者一把梭穿。嗯,这章咱们就把这四个核心参数掰开揉碎讲清楚。

2.1 Nonce:一次性数字的生死线

Nonce 在 CCM 里是绝对的核心。它必须保证:在同一个密钥下,永远不重复。一旦重复,CTR 模式的加密流就会被还原,密文直接裸奔。我在项目中遇到过一起事故,就是因为 Nonce 生成器在进程重启后计数器归零,导致两个会话用了同一个 Nonce,还好是内部测试环境发现的。

⚠ 我曾经踩过的坑: 用时间戳 + 随机数拼接 Nonce,结果时间戳精度不够,高并发下两个请求落在同一毫秒。Nonce 重复,加密直接失效。后来我改用全局原子计数器 + 进程 ID 拼接,才彻底解决。

CCM 标准(RFC 3610)规定 Nonce 长度可以是 7、8、9、10、11、12、13 字节。为什么是这些值?因为 CCM 的格式字段(Flags)只有 1 个字节,其中 3 位用来表示 Nonce 长度。公式是:NonceLen = 15 - L,其中 L 是消息长度字段的字节数(后面会讲)。

Nonce 长度(字节) L 值(消息长度字段字节数) 最大消息长度(字节) 常见场景
7 8 2^64 - 1 极少用,消息极大
11 4 2^32 - 1 物联网、传感器数据
12 3 2^24 - 1 Wi-Fi 加密(CCMP)
13 2 2^16 - 1 短消息、蓝牙

我个人习惯用 12 字节 Nonce。为什么?因为 Wi-Fi 的 CCMP 协议就用这个长度,生态成熟,库支持好。而且 12 字节足够容纳 64 位时间戳 + 32 位计数器,基本不会撞车。

💡 小技巧: Nonce 不要用纯随机数。随机数生成器有碰撞概率,虽然低,但在海量消息下迟早出事。我建议用 时间戳 + 单调递增计数器 的组合,既保证唯一性,又方便调试时追踪消息顺序。

2.2 Associated Data:认证但不加密的数据

Associated Data(AD),也叫附加认证数据。它的特点是:参与 MAC 计算,但不参与加密。说白了,就是你想确保某段数据没有被篡改,但又不想把它藏起来。

典型场景是什么?网络协议头。比如 IP 包的目标地址、端口号。这些字段接收方必须明文读取才能路由,但你又不希望中间人偷偷改掉。AD 就是干这个的。

我做过一个车联网项目,车辆上报 GPS 数据时,把车辆 ID 放在 AD 里,位置数据放在 Plaintext 里加密。这样网关收到后,先看车辆 ID 是否在白名单里,再解密位置。如果攻击者篡改车辆 ID,MAC 校验直接失败,数据丢弃。

🔑 关键点: AD 的长度在 CCM 里是 无上限 的,但实际受限于消息格式字段。标准实现通常支持 0 到 2^64 - 1 字节。不过,AD 越长,MAC 计算越慢,因为 CBC-MAC 要处理所有 AD 块。我建议 AD 长度控制在 64 字节以内,性能最优。

2.3 Plaintext:要加密的明文

Plaintext 就是你想保护的核心数据。CCM 对明文长度有限制,这个限制取决于你选的 Nonce 长度。还记得前面说的 L 值吗?L 越大,能加密的消息越长。

公式很简单:MaxPlaintextLen = 2^(8*L) - 1。举个例子:

  • L = 2(Nonce = 13 字节):最大明文 65535 字节
  • L = 4(Nonce = 11 字节):最大明文 4GB 左右
  • L = 8(Nonce = 7 字节):最大明文 2^64 字节,基本不限

实际项目中,我很少见到需要加密超过 64KB 的场景。大部分物联网报文都在 1KB 以内。所以 L = 2 或 L = 3 就够用了。别为了追求大消息而把 Nonce 缩得太短,Nonce 的唯一性比消息长度重要得多。

⚠ 注意: CCM 的明文长度必须在加密前就确定。它不像 GCM 那样支持流式输入。你必须在调用加密函数时,一次性传入完整的明文。如果数据是分片到达的,你得先缓存再加密。这一点我在做流媒体加密时吃过亏,后来改用 GCM 才解决。

2.4 Tag 长度:安全与效率的权衡

Tag 就是认证标签,也叫 MAC 值。它的作用是验证密文和 AD 的完整性。Tag 越长,安全性越高,但网络开销也越大。

CCM 标准支持 Tag 长度为 4、6、8、10、12、14、16 字节。为什么都是偶数?因为 CCM 的格式字段设计如此,奇数长度不支持。

Tag 长度(字节) 安全等级 推荐场景
4 极低 几乎不用,碰撞概率太高
8 中等 内部网络、低安全要求
12 高 大多数商业应用
16 最高 金融、政府、军事

我个人习惯用 12 字节 Tag。为什么不是 16?因为 12 字节已经提供了 96 位的安全强度,暴力破解需要 2^96 次尝试,这在可预见的未来是安全的。而且 12 字节比 16 字节少 4 字节,对于物联网这种带宽敏感的场景,能省一点是一点。

💡 避坑指南: 我曾经在一个项目中用了 4 字节 Tag,结果被同事审计时骂了一顿。4 字节 Tag 的碰撞概率是 1/2^32,对于百万级消息的系统,碰撞几乎是必然的。攻击者只需要不断发送伪造消息,迟早能撞到一个合法的 Tag。所以,Tag 长度不要低于 8 字节,这是底线。

2.5 参数组合的黄金法则

这四个参数不是孤立的,它们通过 CCM 的格式字节(Flags)紧密耦合。格式字节的结构如下:

Flags = (AdataFlag << 6) | ((M-2) << 3) | (L-1)
其中:
- AdataFlag: 1 表示有 AD,0 表示无 AD
- M: Tag 长度(字节),取值范围 4, 6, 8, 10, 12, 14, 16
- L: 消息长度字段字节数,取值范围 2, 3, 4, 5, 6, 7, 8

举个例子,如果你用 12 字节 Tag(M=12)、2 字节长度字段(L=2)、并且有 AD,那么 Flags 字节就是:

Flags = (1 << 6) | ((12-2) << 3) | (2-1)
      = 0x40 | 0x50 | 0x01
      = 0x51

这个 Flags 字节会被拼在 Nonce 前面,一起参与 CBC-MAC 计算。所以,发送方和接收方的参数必须完全一致,否则 MAC 校验必败。

🔑 黄金法则: 在一个系统中,Nonce 长度、Tag 长度、L 值 这三个参数一旦确定,就不要轻易改动。我建议把它们写在协议文档的第一页,并在代码里用常量定义。我曾经见过一个团队,因为开发环境和生产环境的 Tag 长度不一致,导致联调时花了三天才定位到问题。

2.6 参数选择流程图

下面这张图帮你快速决策:

CCM 参数选择决策流程 开始选择参数 消息最大长度是多少? ≤ 65535 字节 → L=2 ≤ 4GB → L=4 Nonce = 13 字节 Nonce = 11 字节 Tag = 12 字节 Tag = 16 字节 推荐组合 1 推荐组合 2 组合1:Nonce=13, L=2, Tag=12 — 适合物联网、短消息场景 组合2:Nonce=11, L=4, Tag=16 — 适合大文件、高安全场景

2.7 实战代码示例

最后,给一个 Python 的 CCM 参数配置示例,用的是 cryptography 库:

from cryptography.hazmat.primitives.ciphers.aead import AESCCM

# 参数配置
key = b'\x01\x02\x03\x04' * 4          # 16 字节 AES-128 密钥
nonce = b'\x00' * 12                    # 12 字节 Nonce
data = b'Hello, CCM!'                   # 明文
aad = b'\x01\x02'                       # 附加认证数据

# 创建 CCM 对象,指定 Tag 长度为 12 字节
ccm = AESCCM(key, tag_length=12)

# 加密并生成密文 + Tag
ciphertext = ccm.encrypt(nonce, data, aad)

# 解密并验证
decrypted = ccm.decrypt(nonce, ciphertext, aad)
print(decrypted)  # 输出: b'Hello, CCM!'

注意看,AESCCM(key, tag_length=12) 这里指定了 Tag 长度。Nonce 长度由你传入的 nonce 字节数决定。库会自动计算 L 值。如果你传了 12 字节 Nonce,L 就是 3,最大明文 16MB。够用了吧?

⚠ 最后提醒: 解密时如果 Nonce、AD、密文任何一个被篡改,decrypt() 会抛出 InvalidTag 异常。不要自己手动校验 Tag,让库去做。我见过有人自己写 Tag 比较逻辑,结果用了不安全的字符串比较,被时序攻击破解了。