第4章:Associated Data处理——AD是什么?编码规则与常见错误

讲CCM,绕不开Associated Data,简称AD。

很多人觉得AD就是“附带数据”,随便传一下就行。我见过不少项目,加密解密都对了,偏偏AD处理出了岔子,结果认证死活过不去。说白了,AD是CCM安全性的基石之一,处理不好,整个认证加密就形同虚设。

4.1 什么是Associated Data?

AD,全称是Associated Data,中文叫“关联数据”。它的核心特点是:参与认证,但不参与加密。

你想想看,有些数据需要保证完整性,但不需要保密。比如网络包头的某些字段、文件元数据、协议版本号。这些信息如果被篡改,接收方必须能发现,但内容本身没必要加密——因为本来就是公开的。

AD的三大特性:

  • 认证保护:AD会被纳入MAC计算,任何篡改都会被检测到
  • 不加密:AD以明文形式传输,不进行加密处理
  • 长度灵活:可以为0,也可以长达2^64 - 1字节

我个人习惯把AD理解成“防伪标签”。它不藏起来,但谁要是动了它,一眼就能看出来。

4.2 AD的编码规则

AD的编码,在NIST SP 800-38C里有明确规定。我当年第一次看规范时,觉得这规则有点绕。后来踩过坑才明白,每一步都有它的道理。

编码规则分两种情况:

4.2.1 当AD长度为0时

最简单的情况。什么都不用编码,直接跳过。B0块之后就是加密数据。

4.2.2 当AD长度大于0时

这时候需要按照以下规则编码:

  1. 判断长度范围:
    • 如果AD长度在0到2^16 - 2^8之间(即0到65280字节):用2字节表示长度
    • 如果AD长度在2^16 - 2^8到2^32之间:用6字节表示长度,前2字节固定为0xFFFE
    • 如果AD长度在2^32到2^64 - 1之间:用10字节表示长度,前2字节固定为0xFFFF
  2. 编码格式:长度字段 + AD原始数据
  3. 填充要求:整个AD编码块必须按16字节对齐,不足部分补0

我的经验:大多数实际场景中,AD长度都在65280字节以内。所以最常见的编码就是2字节长度 + AD数据 + 可能的填充。别一上来就写复杂的长度判断逻辑,先确认你的AD长度范围。

来看一个具体的编码示例:

# 假设AD = b"Hello, CCM!",长度13字节
# 13 < 65280,所以用2字节表示长度

ad = b"Hello, CCM!"
ad_len = len(ad)  # 13

# 编码:2字节大端长度 + AD数据
encoded_ad = struct.pack(">H", ad_len) + ad  # 共15字节

# 填充到16字节倍数
padding_len = 16 - (len(encoded_ad) % 16)
if padding_len != 16:
    encoded_ad += b'\x00' * padding_len

# 最终encoded_ad长度为16字节
print(encoded_ad.hex())
# 输出:000d48656c6c6f2c2043434d2100000000

注意看:000d就是长度13,后面跟着AD内容,最后补了1个字节的0。

4.3 AD在CCM认证中的位置

AD编码完成后,会被拼接到B0块之后,一起参与CBC-MAC计算。整个认证数据的结构是这样的:

认证输入 = B0块 || 编码后的AD || 密文

B0块是控制信息,AD是关联数据,密文是加密后的载荷。这三部分串起来,一起算MAC。

我画了一张图,帮你理清这个流程:

CCM认证数据组成与MAC计算流程 B0控制块 16字节 编码后的AD 长度可变,16字节对齐 密文数据 长度可变,16字节对齐 CBC-MAC 计算 AES加密链,最后一个密文块作为MAC MAC 认证码 取前M字节作为最终MAC AD参与MAC计算 但以明文传输 接收方需重新编码验证

4.4 常见错误与避坑指南

AD处理看似简单,但坑不少。我这些年做安全审计,发现AD相关的bug占了CCM实现错误的很大比例。

错误一:AD编码时长度字段字节序搞错

规范明确要求使用大端序(Big-Endian)。我见过有人用struct.pack("<H", ad_len),结果长度字段反了,认证死活过不去。

避坑指南:编码AD长度时,务必使用大端序。Python里用struct.pack(">H", ...)或int.to_bytes(length, 2, 'big')。

错误二:AD填充逻辑写错

填充只针对编码后的AD块,不是原始AD数据。我曾经见过一个实现,把原始AD填充到16字节,然后才加长度字段——顺序完全反了。

# 错误写法
padded_ad = ad + b'\x00' * (16 - len(ad) % 16)  # 先填充原始AD
encoded = struct.pack(">H", len(ad)) + padded_ad  # 再加长度

# 正确写法
encoded = struct.pack(">H", len(ad)) + ad  # 先加长度
padding = 16 - len(encoded) % 16
if padding != 16:
    encoded += b'\x00' * padding  # 再填充编码后的整体

错误三:AD长度为0时还编码

当AD长度为0时,规范要求不编码任何内容。有人习惯性地加了个空长度字段,结果多出来2个字节,破坏了MAC计算。

我的建议:写代码时,先判断if ad:,只有非空才走编码逻辑。空AD直接跳过,别画蛇添足。

错误四:接收方AD验证不一致

这是最隐蔽的坑。发送方和接收方对AD的理解必须完全一致。我遇到过这样一个案例:发送方把协议版本号编入AD,接收方却把版本号+保留字段一起编入AD。两边AD不一样,MAC验证永远失败。

避坑指南:AD的构成必须在通信双方之间严格约定。任何一方多一个字节、少一个字节,或者顺序不同,都会导致认证失败。建议在协议文档里明确写出AD的组成规则,并用测试向量验证。

4.5 AD处理的最佳实践

说了这么多坑,我总结几条实用的建议:

实践要点 具体做法 原因
统一编码函数 发送方和接收方共用同一个AD编码函数 避免两边实现不一致
测试向量验证 用NIST提供的CCM测试向量验证AD编码 确保实现与标准一致
AD长度限制 尽量控制在65280字节以内 避免复杂的长度编码逻辑
日志记录 调试时打印AD编码后的hex值 方便对比发送方和接收方的编码结果
协议文档化 明确写出AD包含哪些字段及其顺序 防止理解偏差导致认证失败