NHP 消息头
每个 NHP 数据包都以一段固定长度的消息头开始,其中携带身份、临时密钥、 重放保护状态,以及对消息头本身的 HMAC。本页对每个字段给出字节偏移、 用于在传输中隐藏公开元数据的混淆机制,以及 Go 源码中解析它的位置。
对应 CSA《Stealth Mode SDP》白皮书 §NHP Message Header (Table 3)。常量定义见 nhp/core/scheme/curve/header.go 与 nhp/core/scheme/gmsm/header.go;分发逻辑见 nhp/core/packet.go。
CSA 白皮书给出 160 / 224 字节,对应的是早期版本:此版本头部中唯一的身份 密文材料仅为发送方的静态公钥。当前 Go 实现额外包裹了 80 字节的 IBC 身份 块,因此总长度变为 240 / 304 字节。当规范与代码不一致时,本文档以代码 为准(详见 协议参考 §规范版本)。
布局
- 标准(国际密码套件):240 字节 ——
CIPHER_SCHEME_CURVE(Curve25519、AES-256-GCM、BLAKE2s)。 - 扩展(国密套件):304 字节 ——
CIPHER_SCHEME_GMSM(SM2、SM4-GCM、SM3)。
AEAD 加密后的消息载荷(明文长度 + 16 字节 GCM tag)紧随消息头之后。 整包长度为 header + ciphertext,受 UDP 上限 65,535 字节约束。也支持使 用 TCP 短连接作为替代传输方式。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前导混淆(4,随机,作为 XOR 掩码) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 消息类型(2) ‖ 消息长度(2) —— 受 XOR 掩码保护 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 主版本号(1) | 次版本号(1) | 协议标志(2) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 预留(4,置零) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 计数器,大端(字节 0–3) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 计数器,大端(字节 4–7) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 临时公钥(Curve25519 为 32,SM2 为 64) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| IBC 身份密文(80,AEAD) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 静态公钥密文(48 / 80,AEAD) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 时间戳密文(24,AEAD) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| HMAC(32) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| 加密后的消息载荷 + tag(变长) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段
偏移量和长度使用 Curve / GMSM 组合表示,两种方案存在差异。Curve25519 的临时公钥为 32 字节,SM2 为 64 字节(未压缩的 X‖Y);静态公钥明文 也遵循相同的差异,因此后续字段在两种方案中的偏移不同。
| 偏移 | 大小(字节) | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | 前导混淆 | 每包随机生成的 4 字节,作为一次性 XOR 掩码,遮蔽紧随其后的 4 字节「类型‖长度」字段,使被动观察者无法简单识别包类型或大小。在线上以明文出现;接收方先读取该值,再反混淆。 |
| 4 | 2 | 消息类型 | NHP 消息类型 ID(参见 消息类型)。与消息长度合并打包为一个 32 位大端字后再与前导混淆做 XOR。 |
| 6 | 2 | 消息长度 | 紧随消息头之后的密文长度,包含 16 字节 AEAD tag,大端。与消息类型一起经 XOR 掩码保护。上限为 65,535(UDP 限制)。 |
| 8 | 1 | 协议主版本号 | 明文。收到不支持的主版本时收发方静默丢弃(默认拒绝)。 |
| 9 | 1 | 协议次版本号 | 明文。仅用于向后兼容的递增。 |
| 10 | 2 | 协议标志 | 大端位字段(见下方表格)。低 12 位中的未分配位应为零。第 12 位是密码套件选择器(0 = Curve,1 = GMSM);第 13–15 位保留,与第 12 位一起构成顶端半字节选择器,以便将来扩展新的密码套件。 |
| 12 | 4 | 预留 | 当前发送端均置零;接收端忽略该字段内容。作为向前兼容的占位。 |
| 16 | 8 | 计数器 | 64 位大端,既是随机数(nonce)也是事务跟踪符。计数器占据 12 字节 GCM nonce 的第 4–11 字节;第 0–3 字节全零填充。参见 NonceBytes。每次加密单调递增;接收端拒绝本会话中旧于已见值的计数器。 |
| 24 | 32 或 64 | 临时公钥 | 每包新鲜生成的临时公钥 —— Curve25519/X25519 为 32 字节(标准),SM2 为 64 字节(扩展,未压缩的 X‖Y)。驱动 Noise 握手密钥派生,使得即便是单次消息类型也获得前向保密。 |
| 56 / 88 | 80 | IBC 身份密文 | AEAD 加密的 IBC 身份块:明文为固定 64 字节槽位(MaximumIdentitySize) —— 身份较短时零填充,PKI 模式下全部为零 —— 再附加 16 字节 tag。 |
| 136 / 168 | 48 或 80 | 静态公钥密文 | AEAD 加密的发送方长期静态公钥。使用由临时 DH 派生的密钥解密。总大小 = 明文密钥(32 或 64 字节)+ 16 字节 tag。 |
| 184 / 248 | 24 | 时间戳密文 | AEAD 加密的 8 字节 UNIX 毫秒时间戳 + 16 字节 tag。用于新鲜性校验;接收端施加容忍窗口以抵御重放同时容忍时钟漂移。 |
| 208 / 272 | 32 | HMAC | 对前置所有头字节(偏移 0 起至本字段前一字节为止)的带密钥哈希 —— 即整个消息头 不含 HMAC 自身;不覆盖 载荷密文。NHP-RKN 会在哈希输入末尾追加此前获得的 cookie。在 AEAD 解密 之前 校验;不匹配则静默丢弃,符合默认拒绝原则。参见 MsgAssemblerData.addHMAC。 |
消息头总长:标准 240 字节,扩展 304 字节。
协议标志位(位 0 = LSB)
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | NHP_FLAG_EXTENDEDLENGTH | 置位表示 304 字节的 GMSM 消息头;清零表示 240 字节的 Curve 消息头。 |
| 1 | NHP_FLAG_COMPRESS | 载荷明文在加密前已经过 zlib 压缩。 |
| 2 | NHP_FLAG_CL_PKC | CL-PKC(无证书公钥密码)模式。 |
| 3–11 | — | 预留。 |
| 12 | 密码套件选择器 | 0 = CIPHER_SCHEME_CURVE;1 = CIPHER_SCHEME_GMSM。 |
| 13–15 | — | 预留。与第 12 位一起形成顶端半字节选择器,以便在不破坏字段布局的前提下新增密码套件。 |
权威定义见 nhp/common/packet.go。
混淆机制
线上最前 8 字节 —— 前导混淆与随后的「类型‖长度」组合 —— 是解密前 唯一暴露的面。单次 4 字节 XOR 将「类型」与「长度」一起相对于前导混 淆字做掩码:
preamble = wire[0..4] // 字节 0–3,大端 uint32 随机
type_and_len = wire[4..8] // 字节 4–7,大端 uint32,经 XOR 掩码
decoded = preamble XOR type_and_len
type = (decoded >> 16) & 0xFFFF
length = decoded & 0xFFFF
半开区间 a..b 表示”字节下标 a(含)至下标 b(不含)”,与 Go / Rust 切片语义一致。
这是纵深防御的一层 —— 载荷本身为 AEAD 加密、静态密钥被 AEAD 包 裹 —— 但混淆让简单的流量分析与指纹识别无所适从。参见 nhp/core/scheme/curve/header.go 中的 SetTypeAndPayloadSize / TypeAndPayloadSize。
版本与标志处理
- 版本不匹配 → 静默丢弃。不得针对不支持的主版本作出回应,否则会暴露服务器存在。
- 未知标志位 → 低 12 位中未在上表定义的位应置零。接收方可根据部署策略选择静默丢弃或记录后丢弃。未来 PQC 混合模式将落到协议标志位,而不必升级主版本号。
并发会话
同一 NHP-Agent 发起的并发敲门(例如针对多个被保护资源)通过以下机制区分:
- 每次握手 临时密钥 都是全新的 —— 即便在同一秒内也不会冲突。
- 每个会话拥有 独立 Noise 状态(CipherState + 链式密钥)。
- 载荷层携带 Session / Transaction ID(参见 NHP-ACK 消息)。
发送端无需任何协调或加锁。