gotlcp

DTLCP 数据报传输层密码协议 — 设计文档

1. 概述

1.1 协议定位

DTLCP(Datagram Transport Layer Cryptography Protocol,数据报传输层密码协议)是 GB/T 38636-2020 TLCP(传输层密码协议)在 UDP 传输层上的适配版本,定义于 GM/T 0128-2023。

协议关系链:

协议关系链

DTLCP 相对于 DTLS 1.2 的核心变化是:用国密算法(SM2/SM3/SM4)替换国际算法,保留 DTLS 的 UDP 适配机制(显式序列号、分片重组、无状态 Cookie、超时重传)。

1.2 设计目标

1.3 协议版本

协议 版本号 说明
TLCP {0x01, 0x01} GB/T 38636-2020
DTLCP {0x01, 0x01} GM/T 0128-2023,与 TLCP 相同
DTLS 1.2 {254, 253} RFC 6347,采用 1’s complement 编码

设计决策:DTLCP 使用与 TLCP 相同的版本号 {0x01, 0x01} 而非 DTLS 的版本号方案。DTLCP 的协议区分不依赖版本号,而是通过记录层版本号 + 传输层特征(TCP/UDP)共同判断。


2. 协议栈架构

2.1 分层结构

DTLCP 协议栈分层结构

处理流程:


3. DTLCP 与 TLCP 的核心差异

3.1 差异总览

维度 TLCP DTLCP
传输层 TCP(可靠、有序) UDP(不可靠、无序)
序列号 隐式(64位计数器,不传输) 显式(epoch + sequence_number 字段)
分片 TCP 自动处理 握手消息手动分片(fragment_offset/length)
重传 TCP 保证 自实现状态机(PREPARING/SENDING/WAITING/FINISHED)
DoS 防护 无 无状态 Cookie(HelloVerifyRequest)
重放防护 TCP 序列号天然防护 滑动窗口(默认64,最小32)
消息边界 流式,可跨 TCP 段 单条记录必须在单个 UDP 报文内
多记录 连续流 同一 UDP 报文可含多个记录,连续放置

3.2 握手协议新增字段

新增 说明
message_seq(uint16) 握手消息序号,区分重传与重排序
fragment_offset(uint24) 分片偏移量,支持大消息分片
fragment_length(uint24) 当前分片长度
HelloVerifyRequest 消息 无状态 Cookie 交换,防 DoS
cookie 字段(ClientHello) 客户端携带服务端返回的 Cookie

4. 密码算法与密钥体系

4.1 密码算法

4.1.1 非对称密码算法

4.1.2 分组密码算法

4.1.3 密码杂凑算法

4.1.4 数据扩展函数 P_hash

P_hash(secret, seed) = HMAC(secret, A(1) + seed) +
                        HMAC(secret, A(2) + seed) +
                        HMAC(secret, A(3) + seed) + ...
其中:
  A(0) = seed
  A(i) = HMAC(secret, A(i-1))

P_hash 可无限扩展输出,直到产生所需长度的密钥素材。

4.1.5 伪随机函数 PRF

PRF(secret, label, seed) = P_hash(secret, label + seed)

使用 SM3 作为底层 HMAC 杂凑算法。

4.2 密码套件

优先级 密码套件 密钥交换 加密 校验 编码值
1 ECC_SM4_GCM_SM3 ECC SM4-GCM SM3 {0xe0, 0x13}
2 ECC_SM4_CBC_SM3 ECC SM4-CBC SM3 {0xe0, 0x11}
3 ECDHE_SM4_GCM_SM3 ECDHE SM4-GCM SM3 {0xe0, 0x53}
4 ECDHE_SM4_CBC_SM3 ECDHE SM4-CBC SM3 {0xe0, 0x51}

设计决策:ECC 模式优先于 ECDHE 模式。ECC 模式下服务端加密证书公钥固定,客户端直接使用公钥加密预主密钥,无需额外握手交互。ECDHE 模式提供前向安全性但需要双方交换临时公钥。

4.3 密钥层次结构

密钥层次结构

4.4 密钥种类

密钥类型 说明
服务端密钥 签名密钥对(身份鉴别)+ 加密密钥对(密钥协商)
客户端密钥 签名密钥对(身份鉴别)+ 加密密钥对(密钥协商)
预主密钥 双方协商生成的密钥素材,用于生成主密钥
主密钥 48字节,由预主密钥 + 客户端随机数 + 服务端随机数计算
写密钥 发送方使用的工作密钥(加密 + 校验),分 client_write 和 server_write
读密钥 接收方使用的工作密钥(解密 + 校验),分 client_write 和 server_write

5. 记录层协议

5.1 记录层报文结构

DTLCP 记录层报文在 TLCP 记录头基础上新增两个字段(Epoch + SequenceNumber,共 8 字节),记录头总计 13 字节:

TLCP vs DTLCP 记录层报文对比

字段 大小 说明
Type 1 字节 记录类型:握手(22)、密码规格变更(20)、告警(21)、应用数据(23)
Version 2 字节 协议版本 {0x01, 0x01}
Epoch 2 字节 密码规格变更计数器,区分密钥阶段
SequenceNumber 6 字节(48位) 显式序列号,同 epoch 内单调递增
Length 2 字节 Fragment 长度
Fragment 变长 载荷数据

5.2 epoch 与 sequence_number

epoch 状态转换:

Epoch 状态转换

实现上将 epoch(2B) 与 seq_num(6B) 拼接为 8 字节参与 MAC 和 AEAD 的 AAD 计算,确保每条记录的序列号在网络层面唯一可验证。

5.3 MAC 计算

CBC 模式

MAC = HMAC_hash(
    write_MAC_secret,
    epoch + sequence_number + type + version + length + fragment
)

与 TLCP 关键差异:TLCP 使用隐式的 64 位 seq_num(不传输),DTLCP 使用显式的 epoch + sequence_number 拼接为 64 位值参与 MAC 计算。

AEAD 模式(GCM)

附加鉴别数据(AAD):

additional_data = epoch + sequence_number + type + version + length

与 TLCP 关键差异:TLCP 的 AAD 使用隐式 64 位 seq_num,DTLCP 使用显式的 epoch + sequence_number。

5.4 分片规则

结构 最大 fragment 长度 说明
DTLSPlaintext 2^14 (16384) 与 TLCP 相同,不含头部
DTLSCompressed 2^14 + 1024 压缩后最多膨胀 1024 字节
DTLSCiphertext 2^14 + 2048 加密后最多膨胀 2048 字节

PMTU 感知:发送方应尝试将记录大小控制在 PMTU 范围内,避免 IP 分片。详见 §5.6 PMTU 与路径最大传输单元。

5.5 重放保护

采用滑动窗口机制:

重放保护滑动窗口

参数 说明
最小窗口大小 32
默认窗口大小 64
窗口右边缘 当前会话接收到的最高有效序列号
窗口左边缘 右边缘 - 窗口大小

处理流程:

  1. 接收记录,检查序列号
  2. 序列号 < 窗口左边缘 → 判定为重放,丢弃
  3. 序列号在窗口内且已记录 → 判定为重复,丢弃
  4. 序列号在窗口内且为新 → 进行 MAC 验证
  5. MAC 验证成功 → 更新窗口
  6. MAC 验证失败 → 丢弃记录(不更新窗口)

窗口大小由 Config.ReplayWindow 控制,高丢包、高乱序网络可适当增大(如 128)。

5.6 PMTU 与路径最大传输单元

5.6.1 PMTU 概念

PMTU(Path Maximum Transmission Unit,路径最大传输单元)是指从发送方到接收方之间所有链路中 MTU 的最小值。对于基于 UDP 的 DTLCP 协议,PMTU 直接影响记录层分片策略——UDP 不提供 TCP 的分段重传机制,每个 DTLCP 记录必须完整地放入单个 UDP 报文,而 UDP 报文不能跨越 IP 分片边界。

RFC 6347 §4.1.1 明确规定:

DTLS 记录层协议要求记录不得跨越多个底层数据报(即 UDP 报文)。因此 DTLS 实现必须确保记录大小不超过 PMTU,避免 IP 层分片。

PMTU 对 DTLCP 的影响体现在三个层面:

层面 影响 应对
记录层 应用数据记录必须 ≤ PMTU maxPayloadSizeForWrite() 依据 PMTU 限制单次写入载荷
握手消息 握手消息可能远超 PMTU(如证书链可达数 KB) 自动分片:fragment_offset / fragment_length 字段支持重组
UDP 打包 同一 UDP 报文可含多条短记录 连续放置,总长度 ≤ PMTU

5.6.2 以太网/IP 报文封装层次

在典型以太网环境下,DTLCP 报文从内到外依次被 UDP、IP、以太网帧逐层封装:

 以太网帧(≤ MTU 1500)
┌──────────────────────────────────────────────────────────────┐
│ L2 以太网帧头  14B    目标MAC(6)  源MAC(6)  EtherType(2)    │
├──────────────────────────────────────────────────────────────┤
│ L3 IP 头部  20B        版本·长度·标识·标志·片偏移·TTL·      │
│                        协议·校验和·源IP(4B)·目标IP(4B)       │
├──────────────────────────────────────────────────────────────┤
│ L4 UDP 头部  8B        源端口(2)  目标端口(2)  长度(2)  校验(2)│
├──────────────────────────────────────────────────────────────┤
│ DTLCP 记录头  13B      [Type 1B][Ver 2B][Epoch 2B]          │
│                        [SeqNum 6B][Length 2B]               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│                    DTLCP 载荷(已加密/MAC)                    │
│                    ≤ PMTU = 1400 B                            │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│ L2 以太网 FCS  4B      帧校验序列 (Frame Check Sequence)     │
└──────────────────────────────────────────────────────────────┘
层 头部大小 说明
以太网帧头 14 B 目标 MAC(6) + 源 MAC(6) + EtherType(2)
以太网 FCS 4 B 帧校验序列(Frame Check Sequence)
IP 头部 20 B 不含 IP 选项的标准 IPv4 头部
UDP 头部 8 B 源端口(2) + 目标端口(2) + 长度(2) + 校验和(2)
DTLCP 记录头 13 B Type(1) + Version(2) + Epoch(2) + SeqNum(6) + Length(2)
协议封装总开销 59 B 以太网头(18) + IP头(20) + UDP头(8) + DTLCP头(13)

默认 PMTU = 1400 的推导:

理论最大 DTLCP 载荷 = 以太网 MTU 1500 - 协议开销 59B = 1441 B
预留余量   = 41 B  (IP 选项 + VLAN 标签 802.1Q +4B、PPPoE +8B、隧道封装等)
默认 PMTU  = 1441 - 41 = 1400 B  (保守值,适配绝大多数网络环境)

为什么预留 41 B?互联网环境中路径上可能存在 VLAN 标签(802.1Q 增加 4B)、IP 选项(最大 40B)、IPsec/隧道封装等附加头部。1400 B 是业界广泛采用的保守值,在保证通过率的同时尽量减少 IP 分片概率。

5.6.3 DTLCP 实现中的 PMTU

Config.PMTU 字段(common.go):

type Config struct {
    // PMTU 是路径最大传输单元的估计值。超过此值的记录将被分片。
    // 默认值为 1400 字节。除非确切掌握网络 MTU,否则不应修改。
    PMTU int
}

maxPayloadSizeForWrite() 实现(conn.go):

PMTU 作为单次写入的最大载荷限制,计算逻辑如下:

  1. 取 Config.PMTU,若未设置则用默认值 1400
  2. 扣除 DTLCP 记录头(13 B)和显式 nonce 长度
  3. 扣除加密开销(AEAD overhead 或 CBC MAC 长度)
  4. 结果与协议最大明文长度 maxPlaintext(2^14 = 16384)取较小值
  5. 确保返回值 ≥ 1(极端 PMTU 配置下)

握手消息分片:当构造的握手消息超过 PMTU 时,addHandshake() 自动按 fragment_offset 递增拆分为多个 DTLCP 记录,每个分片的 fragment_length ≤ PMTU。

5.6.4 PMTU 约束与记录打包

PMTU 约束与记录打包规则

三种场景的规则:

场景 A — 记录 ≤ PMTU:记录直接封装为单个 UDP 报文发送,一记录一报文。这是最常见的情况。

场景 B — 记录 > PMTU:记录超出单个 UDP 报文容量,必须拆分为 N 个分片,每个分片 ≤ PMTU。接收方通过 fragment_offset + fragment_length 重组成完整消息。所有分片共享同一 message_seq。

场景 C — 多记录打包:同一 UDP 报文中可连续放置多条 DTLCP 记录,总长度 ≤ PMTU。UDP 载荷首字节必须是 DTLCP 记录头(Type 字段),接收方逐条解析记录头中的 Length 字段确定边界。记录之间无分隔符,也不得跨 UDP 报文。

优先级关系:

PMTU 约束(记录/打包 ≤ PMTU)> 协议最大 fragment 长度(2^14 = 16384 B)

即使协议允许最大 16384 B 的 plaintext,实际发送时仍受 PMTU 约束。当 PMTU < 16384 时以 PMTU 为准;当 PMTU > 16384 时以协议上限为准。在 DTLCP 中,16384 B 的协议上限远大于默认 PMTU 1400 B,因此 PMTU 是实际生效的限制。

5.7 PMTU 变化与动态适应

当前实现使用静态 PMTU(Config.PMTU),不支持 PMTU 动态发现(Path MTU Discovery,RFC 4821)。

设计考虑:

方案 优势 劣势 当前选择
静态 PMTU(当前) 实现简单,无额外网络开销 路径变化时可能 IP 分片或带宽浪费 ✓
ICMP PMTUD(RFC 1191) 自动发现,准确 ICMP 常被防火墙拦截;UDP 无连接,ICMP 不可靠 ✗
DPLPMTUD(RFC 8899) 不依赖 ICMP,适合 UDP 实现复杂,需探测算法 ✗

PMTU 变化时的处理:

参考 RFC 6347 §4.1.1.1:DTLS 实现可在握手阶段尝试 PMTU 发现,使用较小的记录探测路径,根据丢包情况调整。


6. 握手协议

6.1 握手消息头部

DTLCP 在 TLCP 握手消息头部基础上新增 3 个字段,支持 UDP 环境下的乱序到达和分片重组:

字段 大小 说明
msg_type 1 字节 消息类型(见下方类型列表)
length 3 字节 原始消息总长度(重组后)
message_seq 2 字节 DTLCP 新增:消息序号,用于区分重传与重排序
fragment_offset 3 字节 DTLCP 新增:当前分片在原始消息中的偏移量
fragment_length 3 字节 DTLCP 新增:当前分片的数据长度

消息类型包括:HelloRequest(0)、ClientHello(1)、ServerHello(2)、HelloVerifyRequest(3)(DTLCP 新增)、Certificate(11)、ServerKeyExchange(12)、CertificateRequest(13)、ServerHelloDone(14)、CertificateVerify(15)、ClientKeyExchange(16)、Finished(20)。

HelloVerifyRequest 是 DTLCP 相对于 TLCP 唯一新增的消息类型,用于无状态 Cookie 交换。

6.2 message_seq 维护规则

6.3 握手消息分片与重组

当握手消息超过 PMTU 时,发送方将消息分为 N 个连续分片:

握手消息分片示意

关键规则:

实现上通过分片重组缓冲区完成:接收到分片后按 fragment_offset 缓存,直到所有分片收齐后拼接为完整消息;能处理重叠的分片序列(发送方可能在 PMTU 变化后使用更小的分片重传)。

6.4 完整握手流程

DTLCP 完整握手流程

* 表示可选消息,[] 表示不属于握手协议消息。

6.5 会话重用流程

DTLCP 会话重用流程

6.6 握手消息详解

ClientHello

HelloVerifyRequest

Cookie 生成算法:

Cookie = HMAC-SM3(Secret, Client-IP + Client-Parameters)

其中 Client-Parameters 包含 ClientHello 中的版本、随机数、会话ID、密码套件、压缩算法。

Server Certificate

密钥交换算法 证书密钥类型
ECC ECC 公钥,使用加密证书中的公钥
ECDHE ECC 公钥,签名证书用于签名临时公钥

ServerKeyExchange

当密钥交换算法需要额外参数时发送(ECDHE 需临时公钥,ECC 不需)。

Finished

校验数据生成:

verify_data = PRF(master_secret, finished_label,
                  SM3(handshake_messages))[0..11]

7. Flight 设计

7.1 协议规定的 6 个 Flight

DTLCP 握手协议将消息分组为 6 个独立 Flight(飞行),每个 Flight 是一组需连续发送的握手消息:

Flight 发送方 消息组成 Epoch
Flight 1 Client ClientHello (cookie=空) 0
Flight 2 Server HelloVerifyRequest (含 Cookie) 0
Flight 3 Client ClientHello (带 Cookie) 0
Flight 4 Server ServerHello + Certificate + ServerKeyExchange* + CertificateRequest* + ServerHelloDone 0
Flight 5 Client Certificate* + ClientKeyExchange + CertificateVerify* + [ChangeCipherSpec] + Finished 0→1
Flight 6 Server [ChangeCipherSpec] + Finished 1

Flight 5 和 Flight 6 中各含一条 ChangeCipherSpec(不属于握手消息),标志着 epoch 从 0 翻转为 1。

7.2 实现机制

每个 Flight 通过 buffering(缓冲)+ flush(刷新) 机制实现,核心思路是”先累积,再一次性发送”:

Flight 缓冲发送机制

设计意图:Flight 的多条消息需作为一个原子单元处理——要么全部到达对端,要么全部重传。缓冲机制确保同一 Flight 的消息不会被其他数据插入打断,flush 确保底层 UDP 报文连续发出。

7.3 flightData 快照与重传

发送 Flight 后,将 sendBuf 的完整字节快照保存为 flightData。重传时直接原样写入 UDP socket,不重新序列化。

为什么用快照而非重新序列化?

  1. 快照已包含 DTLCP 记录头(epoch + seq_num 已编码),与原始发送完全一致
  2. 避免重复 marshaling 开销
  3. 保证重传内容与首次发送逐字节相同——若重新序列化,随机数字段等可能变化,导致 Finished 校验值不一致

实现要点:快照保存时机必须在 flush() 之前,因为 flush 会清空 sendBuf。快照保存的是完整 sendBuf(含所有记录头和分片),重传时直接 WriteTo(flightData, remoteAddr)。

7.4 Flight 与分片的关系

一个 Flight 中的握手消息可能超過 PMTU,需分片传输:

Flight 与分片关系示例

7.5 Flight 与 Epoch 的区别

维度 Flight Epoch
所处层级 握手状态机(逻辑层) 记录层(协议格式)
可见性 实现内部概念,不出现在网络 记录头中显式传输(2字节)
用途 控制发送/等待/重传节奏 标识密钥阶段,区分明文/密文
取值 Flight 1~6 0(握手)、1(CCS 后)
变化时机 每轮握手消息发送后 每次 ChangeCipherSpec 后
关系 多对一:Flight 1~5 的 epoch 均为 0,Flight 6 的 epoch 为 1  

一句话:Flight 是握手层”什么时候发”的节奏控制,Epoch 是记录层”用什么密钥”的标签。


8. 四态握手状态机

8.1 状态定义

DTLCP 握手状态机有四个状态,专门为 UDP 不可靠传输设计:

状态 职责
PREPARING 构造本轮待发送的握手消息(Flight),缓存到发送队列
SENDING 将 Flight 的消息序列化并发送。若为最后一轮则转入 FINISHED,否则转入 WAITING
WAITING 等待对端响应。三种退出路径:收到正确消息、超时重传、检测到对端重传
FINISHED 握手完成。保持 2×MSL 时间以响应对端最后一个 Flight 的重传

设计意图:与 TLCP 不同,DTLCP 无法依赖 TCP 的有序可靠传输。状态机将”发送”和”等待”显式分离,SENDING 负责构造网络报文,WAITING 负责处理 UDP 的丢包/乱序/重复,两者通过超时和对端重传信号互相切换。

8.2 状态流转

                      +-----------+
                      | PREPARING |
                +---> |           | <--------------------+
                |     |           |                      |
                |     +-----------+                      |
                |           |                            |
                |           | Buffer next flight         |
                |           |                            |
                |          \|/                           |
                |     +-----------+                      |
                |     |           |                      |
                |     |  SENDING  |<------------------+  |
                |     |           |                   |  | Send
                |     +-----------+                   |  | HelloRequest
        Receive |           |                         |  |
           next |           | Send flight             |  | or
         flight |  +--------+                         |  |
                |  |        | Set retransmit timer    |  | Receive
                |  |       \|/                        |  | HelloRequest
                |  |  +-----------+                   |  | Send
                |  |  |           |                   |  | ClientHello
                +--)--|  WAITING  |-------------------+  |
                |  |  |           |   Timer expires   |  |
                |  |  +-----------+                   |  |
                |  |         |                        |  |
                |  |         |                        |  |
                |  |         +------------------------+  |
                |  |                Read retransmit      |
        Receive |  |                                     |
           last |  |                                     |
         flight |  |                                     |
                |  |                                     |
               \|/\|/                                    |
                                                         |
            +-----------+                                |
            |           |                                |
            | FINISHED  | -------------------------------+
            |           |
            +-----------+
                 |  /|\
                 |   |
                 |   |
                 +---+

              Read retransmit
           Retransmit last flight

8.3 状态转换规则

触发条件 状态转换 行为
重传定时器超时 WAITING → SENDING 重发当前 Flight,backoff() 翻倍超时值,回到 WAITING
检测到对端重传 WAITING → SENDING 重发当前 Flight,backoff() 翻倍超时值,回到 WAITING
收到下一轮消息 WAITING → PREPARING 非最后一轮时,构造新的 Flight 消息
收到最后一轮消息 WAITING → FINISHED 握手完成,保持 2×MSL 响应对端重传
发送完毕(最后一轮) SENDING → FINISHED 最后一轮消息发送完成,不再等待
发送 / 收到 HelloRequest SENDING → PREPARING 对端请求重新握手
FINISHED 收到重传 FINISHED → SENDING 重传最后一个 Flight

8.4 死锁预防


9. 重传定时器

9.1 数据结构

RetransmitTimer 维护一个动态超时值,支持指数退避,通过工厂函数注入以便测试 mock:

字段 说明
initial 初始超时值(固定不变,默认 1s)
current 当前超时值(每次 backoff 翻倍,reset 恢复为 initial)
max 超时上限(默认 64s,退避到达后不再增长)
factory 定时器工厂函数,默认用 time.NewTimer,测试时替换为 mock

9.2 操作

方法 行为
start() 用 current 值创建新定时器
backoff() current *= 2,上限 max,然后 start()
reset() current = initial,然后 start()
stop() 停止并清空 handle
fired() 非阻塞检查定时器是否已触发(select/default)

9.3 指数退避序列

1s → 2s → 4s → 8s → 16s → 32s → 64s → 64s → ...

超过 MaxRetransmitTimeout(默认 64s)后不再增长。

9.4 在握手状态机中的集成

发送 Flight 后:设置状态为 WAITING,调用 reset() 从初始值启动定时器。

等待循环:通过 SetReadDeadline(now + current) 设置 socket 读超时为当前退避值。两种处理:

服务端 readNextFlightMsg:循环内先检查 fired(),若定时器到期则重发 flightData 并 backoff();若检测到对端重传(如重复 ClientHello),同样重发但执行 backoff() 而非 reset()——因为对端重传意味着网络仍不稳定,退避而非重置更安全。

9.5 配置参数

参数 默认值 说明
InitialRetransmitTimeout 1s 初次重传等待时间
MaxRetransmitTimeout 64s 退避上限
NewTimer defaultNewTimer 定时器工厂,测试时可注入 mock

调优建议:

网络环境 InitialRetransmitTimeout MaxRetransmitTimeout
局域网(低延迟、低丢包) 300ms ~ 500ms 8s
广域网(互联网) 1s ~ 2s 60s
高丢包/卫星链路 500ms ~ 1s 60s

10.1 问题分析

基于 UDP 的协议易受两类 DoS 攻击:

  1. 放大攻击:攻击者伪造源 IP 发送 ClientHello,服务端响应大量数据(证书链等)到受害者
  2. 资源耗尽:攻击者发送大量 ClientHello 迫使服务端维护大量半连接状态

无状态 Cookie 防 DoS 攻击

Cookie 生成:

Cookie = HMAC-SM3(Secret, ClientAddr || ClientParams)

其中:

version(2B) || random(32B) || sessionId(1B长度前缀+数据) || cipherSuites(2B长度前缀+每项2B) || compressionMethods(1B长度前缀+数据)

为什么含端口号? UDP 环境下,同一 IP 可能有多个客户端实例(不同端口),Cookie 绑定到具体 IP:Port 可区分同一主机上的不同连接。

Cookie 验证:服务端收到带 Cookie 的 ClientHello 后,用相同方式重新计算 expected,与收到的 Cookie 进行常数时间比较(防时序侧信道攻击),匹配则继续握手,不匹配则重新发送 HelloVerifyRequest。

服务端不存储 Cookie,通过实时计算和比对验证,全程无状态。

10.4 安全保证

攻击类型 防御效果
源IP伪造放大攻击 攻击者必须能接收 Cookie 响应才能继续握手
半连接资源耗尽 Cookie 验证通过前服务端不分配任何连接状态
Cookie 重放 服务端定期更换 Secret,旧 Cookie 过期失效

11. 密钥生命周期与安全

11.1 密钥安全擦除

所有密钥材料在使用后必须安全置零,确保不会残留在内存中:

  1. 预主密钥(preMasterSecret):主密钥生成后调用 setZero 置零(反复写 0xFF + 0x00 各 3 遍 + 内存屏障)
  2. 主密钥(masterSecret):工作密钥派生完成后置零
  3. 工作密钥(MAC key、加密 key、IV):连接 Close() 时置零

11.2 密钥计算

主密钥

master_secret = PRF(pre_master_secret, "master secret",
                    ClientHello.random + ServerHello.random)[0..47]

输入:预主密钥(48字节)+ 标签 + 客户端随机数(32字节)+ 服务端随机数(32字节) 输出:主密钥(48字节)

工作密钥

key_block = PRF(SecurityParameters.master_secret, "key expansion",
                SecurityParameters.server_random +
                SecurityParameters.client_random)

从 key_block 按顺序切分:

client_write_MAC_secret[hash_size]
server_write_MAC_secret[hash_size]
client_write_key[key_material_length]
server_write_key[key_material_length]
client_write_IV[fixed_iv_length]
server_write_IV[fixed_iv_length]

注意:PRF 的 seed 参数中 server_random 在前,client_random 在后(与主密钥计算顺序不同)。

11.3 SM4 密钥长度

算法 key_material_length fixed_iv_length 说明
SM4-GCM 16 字节 4 字节 IV 共 12 字节:4 字节固定 IV + 8 字节显式 nonce
SM4-CBC 16 字节 16 字节 完整 16 字节 IV

附录 A:与 DTLS 1.2 的差异对照

方面 DTLS 1.2 (RFC 6347) DTLCP (GM/T 0128-2023)
版本号 {254, 253}(1’s complement) {0x01, 0x01}
非对称算法 RSA/ECDSA SM2(ECC/ECDHE)
分组密码 AES(GCM/CBC) SM4(GCM/CBC)
杂凑算法 SHA-256 SM3
PRF P_SHA256 P_SM3
签名算法 RSA-SHA256, ECDSA-SHA256 SM2WithSM3 (0x0704)
双证书 不需要 需要(签名证书 + 加密证书)
HelloVerifyRequest 版本 DTLS 1.0 {254, 255} 与协议版本相同 {0x01, 0x01}
Cookie 最大值 255 字节 255 字节
重传定时器最大值 60 秒 64 秒
重放窗口默认值 64 64

附录 B:参考资料