NHP 消息头

每个 NHP 数据包都以一段固定长度的消息头开始,其中携带身份、临时密钥、 重放保护状态,以及对消息头本身的 HMAC。本页对每个字段给出字节偏移、 用于在传输中隐藏公开元数据的混淆机制,以及 Go 源码中解析它的位置。

对应 CSA《Stealth Mode SDP》白皮书 §NHP Message Header (Table 3)。常量定义见 nhp/core/scheme/curve/header.gonhp/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);静态公钥明文 也遵循相同的差异,因此后续字段在两种方案中的偏移不同。

偏移大小(字节)字段说明
04前导混淆每包随机生成的 4 字节,作为一次性 XOR 掩码,遮蔽紧随其后的 4 字节「类型‖长度」字段,使被动观察者无法简单识别包类型或大小。在线上以明文出现;接收方先读取该值,再反混淆。
42消息类型NHP 消息类型 ID(参见 消息类型)。与消息长度合并打包为一个 32 位大端字后再与前导混淆做 XOR。
62消息长度紧随消息头之后的密文长度,包含 16 字节 AEAD tag,大端。与消息类型一起经 XOR 掩码保护。上限为 65,535(UDP 限制)。
81协议主版本号明文。收到不支持的主版本时收发方静默丢弃(默认拒绝)。
91协议次版本号明文。仅用于向后兼容的递增。
102协议标志大端位字段(见下方表格)。低 12 位中的未分配位应为零。第 12 位是密码套件选择器(0 = Curve,1 = GMSM);第 13–15 位保留,与第 12 位一起构成顶端半字节选择器,以便将来扩展新的密码套件。
124预留当前发送端均置零;接收端忽略该字段内容。作为向前兼容的占位。
168计数器64 位大端,既是随机数(nonce)也是事务跟踪符。计数器占据 12 字节 GCM nonce 的第 4–11 字节;第 0–3 字节全零填充。参见 NonceBytes。每次加密单调递增;接收端拒绝本会话中旧于已见值的计数器。
2432 或 64临时公钥每包新鲜生成的临时公钥 —— Curve25519/X25519 为 32 字节(标准),SM2 为 64 字节(扩展,未压缩的 X‖Y)。驱动 Noise 握手密钥派生,使得即便是单次消息类型也获得前向保密。
56 / 8880IBC 身份密文AEAD 加密的 IBC 身份块:明文为固定 64 字节槽位(MaximumIdentitySize) —— 身份较短时零填充,PKI 模式下全部为零 —— 再附加 16 字节 tag。
136 / 16848 或 80静态公钥密文AEAD 加密的发送方长期静态公钥。使用由临时 DH 派生的密钥解密。总大小 = 明文密钥(32 或 64 字节)+ 16 字节 tag。
184 / 24824时间戳密文AEAD 加密的 8 字节 UNIX 毫秒时间戳 + 16 字节 tag。用于新鲜性校验;接收端施加容忍窗口以抵御重放同时容忍时钟漂移。
208 / 27232HMAC对前置所有头字节(偏移 0 起至本字段前一字节为止)的带密钥哈希 —— 即整个消息头 不含 HMAC 自身;不覆盖 载荷密文。NHP-RKN 会在哈希输入末尾追加此前获得的 cookie。在 AEAD 解密 之前 校验;不匹配则静默丢弃,符合默认拒绝原则。参见 MsgAssemblerData.addHMAC

消息头总长:标准 240 字节,扩展 304 字节。

协议标志位(位 0 = LSB)

名称含义
0NHP_FLAG_EXTENDEDLENGTH置位表示 304 字节的 GMSM 消息头;清零表示 240 字节的 Curve 消息头。
1NHP_FLAG_COMPRESS载荷明文在加密前已经过 zlib 压缩。
2NHP_FLAG_CL_PKCCL-PKC(无证书公钥密码)模式。
3–11预留。
12密码套件选择器0 = CIPHER_SCHEME_CURVE1 = 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 发起的并发敲门(例如针对多个被保护资源)通过以下机制区分:

  1. 每次握手 临时密钥 都是全新的 —— 即便在同一秒内也不会冲突。
  2. 每个会话拥有 独立 Noise 状态(CipherState + 链式密钥)。
  3. 载荷层携带 Session / Transaction ID(参见 NHP-ACK 消息)。

发送端无需任何协调或加锁。

另见

  • 消息类型 —— 消息头解析完成后各 ID 的处理方式
  • 加密算法 —— AEAD 与 Noise 所依赖的算法细节
  • 术语表 —— 计数器临时密钥HMAC密码套件 的定义