MAVLink 安全通信系列(3):AES-GCM 与 MAVLink 集成
返回系列总目录。
本篇不重复推导 AES/GHASH,而是把它落地到 MAVLink v2 链路上:加密帧长什么样、
ENCRYPTED位怎么打、 打包与解析状态机怎么走、nonce 和 tag 塞在哪几个字节、代码怎么接入。对应项目文件:
src/c_library_v2/mavlink_aes_gcm.h(算法核心)、mavlink_types.h(帧字段 / 标志位 / 状态)、mavlink_helpers.h(收发接入点)。 编译宏:MAVLINK_USE_AES_ENCRYPTION。
1. 设计目标与范围
在 MAVLink 上做加密,四条硬约束决定了实现形状:
- 只加密 payload,其余字段明文:header 必须保持可解析以支持路由;但明文 header 同时会泄漏元数据;
- 只支持 MAVLink v2:v1 没有
incompat_flags字段,无法声明"加密帧"; - 向后兼容:未定义
MAVLINK_USE_AES_ENCRYPTION宏时,编译产物与原生 MAVLink 逐字节一致,非加密帧走原有路径; - 接收端自动识别:靠 header 里的一位标志位区分"这帧要不要解密"。
最终方案:给加密帧在尾部追加 12 字节 nonce + 16 字节 tag(共 28 字节),并 在 incompat_flags 里置 ENCRYPTED 位。
2. 加密帧结构
普通 v2 帧和加密帧的对比:
普通帧: header │ payload │ CRC │ signature(可选)
加密帧: header │ payload(密文) │ CRC │ signature(可选) │ nonce(12B) │ tag(16B)
└──────── 加密追加部分 ────────┘完整布局(含字段属性):
┌───────────┬──────────────┬─────┬───────────┬──────────────┬─────────────┐
│ header │ payload │ CRC │ signature │ nonce │ auth_tag │
│ (10B 明文)│ (N B,密文) │(2B) │ (可选 13B)│ (12B 明文) │ (16B 明文) │
└───────────┴──────────────┴─────┴───────────┴──────────────┴─────────────┘
← 原始 MAVLink v2 帧 → ← 加密追加部分(28 B)→
← CRC 覆盖范围(header[1..9] + payload + crc_extra,与原始一致)→逐字段表:
| 字段 | 偏移 | 大小 | 属性 | 说明 |
|---|---|---|---|---|
| header.magic | 0 | 1 | 明文 | 0xFD,v2 起始标记 |
| header.len | 1 | 1 | 明文 | payload 长度 N,不含 nonce/tag |
| header.incompat_flags | 2 | 1 | 明文 | 0x02 = MAVLINK_IFLAG_ENCRYPTED |
| header.compat_flags | 3 | 1 | 明文 | 当前 0x00 |
| header.seq | 4 | 1 | 明文 | 包序列号 |
| header.sysid / compid | 5 / 6 | 1 / 1 | 明文 | 系统 / 组件 ID |
| header.msgid | 7 | 3 | 明文 | 消息 ID,不参与加密 |
| payload | 10 | N | 密文 | AES-CTR 加密后的应用数据 |
| CRC | 10+N | 2 | 明文 | 仅覆盖 header[1..9] + payload + crc_extra |
| signature | 12+N | 13 | 明文 | 可选(加密后可关闭,GCM 已提供认证) |
| aes.nonce | 25+N | 12 | 明文 | 随机 nonce,初始化 GCM 计数器 |
| aes.auth_tag | 37+N | 16 | 明文 | GCM 认证标签 |
几个关键设计点:
len仍是原始 payload 长度,不含 nonce/tag——这样不破坏现有长度逻辑;- CRC 行为不变,nonce/tag 在 CRC 之后解析、不参与 CRC;
- nonce/tag 追加在 signature 之后,放在帧最末尾,接收端"读完才解密";
- AAD 是有明确边界的:当前实现只认证
incompat_flags、compat_flags、seq、sysid、compid这 5 个字节;magic、len和 3 字节msgid不在 AAD 中。不能将其概括成“整个 header 都已认证”。 - CRC 仍只是检错码,且本设计的 CRC 不覆盖 nonce/tag;CRC 通过不代表帧通过 密码学认证。
重要协议边界:
msgid和len未纳入当前 GCM AAD,因而不能把 GCM 标签 视为对完整 MAVLink 头的认证。恶意中间人可以修改未被认证的头字段并重算 CRC; 在部署前应扩展 AAD,使协议语义相关的头字段(至少长度与消息 ID)也被认证, 并通过帧格式/兼容性测试验证双端一致。当前章节描述的是仓库实现现状,而不是 对完整 MAVLink 帧认证的保证。
2.1 ENCRYPTED 标志位
在 mavlink_types.h 里新增一位:
#define MAVLINK_IFLAG_SIGNED 0x01 /* 已有:v2 签名 */
#define MAVLINK_IFLAG_ENCRYPTED 0x02 /* 新增:加密帧 */
#define MAVLINK_IFLAG_MASK 0x03 /* SIGNED | ENCRYPTED */只要 incompat_flags & 0x02 为真,接收端就知道"读完之后再去读 nonce + tag, 然后解密"。
3. 发送侧:打包流程
发送一条加密帧的完整流水线:
flowchart TB
A["mavlink_msg_xxx_pack<br/>填充业务结构"] --> B["mavlink_finalize_message_buffer"]
B --> B1["1. GCM 加密 payload(原地)<br/>生成 nonce + auth_tag"]
B1 --> B2["2. 置位 incompat_flags |= ENCRYPTED"]
B2 --> B3["3. 组装 header(明文)"]
B3 --> B4["4. 计算 CRC(header+payload+crc_extra)"]
B4 --> C["mavlink_msg_to_send_buffer"]
C --> C1["5. 序列化 header → payload → CRC → sig → nonce → tag"]
C1 --> D["_mav_finalize_message_chan_send<br/>交给串口 / 数传发送"]用文字串一下:先加密 payload → 打 ENCRYPTED 位 → 组 header → 算 CRC → 串行化时把 nonce/tag 追加到最末尾 → 发出。发送顺序恒为:
header → payload(密文) → CRC → signature(可选) → nonce → tag对应调用链:
mavlink_msg_xxx_pack()
→ mavlink_finalize_message_buffer() // 加密 payload、设 ENCRYPTED 位、算 CRC
→ mavlink_aes_gcm_encrypt() // GCM 加密 + 生成 nonce/tag,并构建 AAD
→ mavlink_msg_to_send_buffer() // 串行化 header|payload|CRC|sig|nonce|tag
→ _mav_finalize_message_chan_send() // 过 UART/数传发出4. 接收侧:解析状态机
MAVLink 接收是逐字节状态机。加密只动了 CRC 之后的两个状态:
stateDiagram-v2
[*] --> IDLE
IDLE --> GOT_STX
GOT_STX --> GOT_LENGTH
GOT_LENGTH --> GOT_INCOMPAT_FLAGS
GOT_INCOMPAT_FLAGS --> GOT_COMPAT_FLAGS
GOT_COMPAT_FLAGS --> GOT_SEQ
GOT_SEQ --> GOT_SYSID
GOT_SYSID --> GOT_COMPID
GOT_COMPID --> GOT_MSGID1
GOT_MSGID1 --> GOT_MSGID2
GOT_MSGID2 --> GOT_MSGID3
GOT_MSGID3 --> GOT_PAYLOAD
GOT_PAYLOAD --> GOT_CRC1
GOT_CRC1 --> GOT_CRC2
GOT_CRC2 --> SIGNATURE_WAIT : SIGNED 置位
GOT_CRC2 --> GOT_AES_NONCE : ENCRYPTED 置位
GOT_CRC2 --> [*] : 否则完成
SIGNATURE_WAIT --> GOT_AES_NONCE : 读完签名且 ENCRYPTED
SIGNATURE_WAIT --> [*] : 读完签名结束
GOT_AES_NONCE --> GOT_AES_TAG : 读 12 字节 nonce
GOT_AES_TAG --> [*] : 读 16 字节 tag,GCM 解密新增的两个状态:
| 状态 | 读取内容 | 完成后 |
|---|---|---|
GOT_AES_NONCE | 12 字节 nonce | → GOT_AES_TAG |
GOT_AES_TAG | 16 字节 auth_tag | GCM 解密 + 校验 → 返回消息 |
解密时机:在 GOT_AES_TAG 读完 16 字节后,一次性做 GCM 解密与 tag 校验:
GOT_AES_TAG(读完 16 字节):
CRC OK 且 aes_gcm 已配置 → mavlink_aes_gcm_decrypt()
→ 重算 tag 并常量时间比对
→ 失败:清零 payload,当作坏帧丢弃(返回 0,上层无感)
→ 成功:返回已解密 payload关键语义:凡能通过加密解析路径到达应用层,payload 已通过 GCM 标签校验。 这只覆盖密文与上述 5 字节 AAD;它不意味着完整 header 已认证,也不意味着 接收端做了 anti-replay 检查。
5. 代码实现拆解
5.1 mavlink_aes_gcm.h —— 算法核心
新增的完整 AES-128-GCM 软件实现,内容与上一篇的原理一一对应:
- AES-128 核心:
_mav_aes_sbox[256]、密钥扩展(176 字节轮密钥)、块加密; - GF(2^128) 乘法:
_mav_aes_gf128_mul(),GHASH 的基础运算(域多项式x^128+x^7+x^2+x+1,约减常数R = 0xE1 << 120); - GCM 上下文:
_mav_aes_gcm_encrypt()/_mav_aes_gcm_decrypt(); - MAVLink 集成接口:
mavlink_aes_gcm_encrypt()—— 加密 payload,生成 nonce 与 tag;mavlink_aes_gcm_decrypt()—— 解密 payload,常量时间验证 tag;mavlink_aes_gcm_build_aad()—— 从 header 构建 AAD;
- 随机源:若未配置
random_callback,会退回 xorshift32;该 PRNG 不具备密码学 安全性,只能用于非安全演示,不能用于生产 nonce 或 ECDHE 私钥。
GCM 的关键常量:
#define MAVLINK_AES_KEY_LEN 16 /* AES-128 */
#define MAVLINK_AES_BLOCK_LEN 16
#define MAVLINK_AES_NONCE_LEN 12 /* 随帧传输的 nonce */
#define MAVLINK_AES_TAG_LEN 16 /* 认证标签 */
#define MAVLINK_AES_ROUNDS 105.2 mavlink_types.h —— 帧字段与状态
/* mavlink_message_t 增加两个 per-packet 字段 */
uint8_t aes_nonce[12];
uint8_t aes_tag[16];
/* mavlink_status_t 增加通道 GCM 状态指针 */
mavlink_aes_gcm_state_t *aes_gcm;
/* 新增标志位 + 解析状态 */
#define MAVLINK_IFLAG_ENCRYPTED 0x02
// 新增 MAVLINK_PARSE_STATE_GOT_AES_NONCE / GOT_AES_TAG
/* 帧长上限:加密帧多 28 字节 */
#define MAVLINK_MAX_PACKET_LEN 308 /* 原 280 → 加密 +28 */5.3 mavlink_helpers.h —— 收发接入点
发送侧改动集中在三处(mavlink_finalize_message_buffer、 _mav_finalize_message_chan_send、mavlink_msg_to_send_buffer);接收侧新增 GOT_AES_NONCE / GOT_AES_TAG 两个状态。所有改动都用 #ifdef MAVLINK_USE_AES_ENCRYPTION 包裹,未启用时产物与原始代码完全一致。
6. 使用方式:从初始化到收发
6.1 初始化加密状态
#define MAVLINK_USE_AES_ENCRYPTION
#include "mavlink_helpers.h"
/* 16 字节预共享密钥(收发双方必须一致) */
static const uint8_t pre_shared_key[16] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
};
/* 生产环境必须注入经过评估的密码学随机源 */
static uint8_t hw_random_callback(uint8_t *buf, uint8_t len) {
for (uint8_t i = 0; i < len; i++)
buf[i] = (uint8_t)hardware_rng_read();
return len;
}
mavlink_aes_gcm_state_t aes_state;
void aes_init(void) {
memcpy(aes_state.key, pre_shared_key, 16);
aes_state.flags = MAVLINK_AES_FLAG_ENCRYPT_OUTGOING; /* 发侧加密 */
aes_state.random_callback = hw_random_callback; /* 生产接 TRNG/合格 DRBG */
mavlink_get_channel_status(MAVLINK_COMM_0)->aes_gcm = &aes_state; /* 绑定通道 */
}6.2 发送加密消息
与应用层普通 MAVLink 用法完全一致,加密自动发生:
void send_encrypted_heartbeat(void) {
mavlink_message_t msg;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
mavlink_msg_heartbeat_pack(
MAV_SYSTEM_ID, MAV_COMP_ID, &msg,
MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_GENERIC,
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, 0, MAV_STATE_ACTIVE);
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg); /* 自动加密 + 加 nonce/tag */
uart_send(buf, len);
}6.3 接收并解密
void receive_encrypted_message(uint8_t byte) {
mavlink_message_t msg;
mavlink_status_t r_status;
if (mavlink_parse_char(MAVLINK_COMM_0, byte, &msg, &r_status)) {
/* 到这里 msg.payload 已解密且通过认证 */
switch (msg.msgid) {
case MAVLINK_MSG_ID_HEARTBEAT: {
mavlink_heartbeat_t hb;
mavlink_msg_heartbeat_decode(&msg, &hb);
break;
}
}
}
}6.4 多通道混合(部分加密、部分明文)
每个通道独立一份 mavlink_aes_gcm_state_t,想哪个通道加密就给它绑状态:
void multi_channel_setup(void) {
mavlink_aes_gcm_state_t aes0;
memcpy(aes0.key, key_for_ch0, 16);
aes0.flags = MAVLINK_AES_FLAG_ENCRYPT_OUTGOING;
mavlink_get_channel_status(MAVLINK_COMM_0)->aes_gcm = &aes0; /* 通道 0 加密 */
mavlink_get_channel_status(MAVLINK_COMM_1)->aes_gcm = NULL; /* 通道 1 明文 */
}7. 资源占用与局限
资源占用(纯软件、嵌入式):
| 项 | 数值 |
|---|---|
| Flash | 约 2~4 KB(S-box 256 B + 轮密钥 176 B + GCM/GHASH 逻辑) |
| RAM | mavlink_aes_gcm_state_t 约 40 字节/通道 |
| 帧开销 | 每帧固定 +28 字节(nonce 12B + tag 16B) |
局限与红线:
- 仅 v2 支持:v1 无
incompat_flags; - nonce 不得重用:同一密钥下 nonce 严禁重复;随机源必须具备密码学质量, 并按密钥消息量评估碰撞风险;
- 不提供业务帧抗重放:随机 nonce 只作为 GCM 输入,接收端没有记录已用 nonce 的窗口。同一会话内重放一帧可能再次通过认证,需由上层序号窗口或协议状态拒绝;
- AAD 覆盖不完整:当前不认证
len与msgid,见第 2 节。若这两个字段影响 解析或业务授权,不应把当前格式描述为完整帧完整性保护; - PSK 模式:本层用预共享密钥加密,只解决"机密性 + 完整性",身份认证与 密钥分发不在本层——身份与密钥协商见第 4、5 篇;
- 加密与 signing 并用属于冗余:GCM 已提供认证,同时开 signing 徒增开销。
8. 小结与下一站
- 加密帧 = 普通 v2 帧 + 尾部 28 字节(nonce 12 + tag 16),靠
ENCRYPTED标志位自动识别;这些字节开销之外,协议还必须正确认证影响解析和 授权的头字段; - 发送"先加密再组帧",接收"读 nonce/tag 后解密",非加密帧完全不受影响;
- GCM 为 payload 和选定 AAD 提供机密性与完整性,但当前方案没有业务帧 anti-replay, 且头部认证不完整;身份认证和会话密钥协商由第 4、5 篇讨论。
下一篇《X.509 与身份认证》转向另一层问题: 证书信任如何建立、ECDH 与 ECDSA 各自证明什么;随后第 5 篇再讨论具体握手记录。
