閱讀建議:每個Section先看核心問題,再看設(shè)計解答,最后看實現(xiàn)要點
目錄
- DTLS要解決的根本問題:UDP ≠ TCP
- 為什么DTLS需要Connection ID?(核心設(shè)計)
- 記錄層重新設(shè)計:可變長頭部與序列號加密
- ACK機制:DTLS 1.3最大的協(xié)議創(chuàng)新
- 握手流程與DoS防護(Cookie交換)
- Epoch與密鑰管理
- 分片與重組
- 重傳狀態(tài)機:實現(xiàn)層面的核心
- Key Update與Connection ID更新
- AEAD安全邊界與實現(xiàn)約束
- 實現(xiàn)陷阱與踩坑指南
1. DTLS要解決的根本問題:UDP ≠ TCP
1.1 核心問題:TLS為什么不能直接跑在UDP上?
原文:
"TLS cannot be used directly over datagram transports for the following four reasons"
| # | 問題 | TLS的假設(shè) | UDP的現(xiàn)實 | DTLS的解法 |
|---|---|---|---|---|
| 1 | 隱式序列號 | 包不會丟,序號 implicit 遞增 | 丟包是常態(tài) | 顯式序列號 + Epoch |
| 2 | 握手必須保序 | 握手消息嚴(yán)格按序到達(dá) | 亂序/丟包 | message_seq + 重傳定時器 |
| 3 | 握手消息可能超大 | TCP自動分片重組 | UDP包<1500B | 應(yīng)用層分片 (fragment_offset) |
| 4 | DoS攻擊面 | TCP三次握手已有源地址驗證 | 源IP可偽造 | Cookie交換驗證可達(dá)性 |
開發(fā)理解:TLS是建立在"可靠傳輸"假設(shè)上的。DTLS不是去模擬TCP(早期的SSL over UDP方案試過,延遲巨大),而是在TLS之上做最少必要的修改來適配UDP的不可靠語義。
1.2 DTLS 1.3 vs DTLS 1.2:承上啟下的關(guān)系
TLS 1.1 ──delta──> DTLS 1.0 (RFC 4347)
TLS 1.2 ──delta──> DTLS 1.2 (RFC 6347) ← 被廢棄
TLS 1.3 ──delta──> DTLS 1.3 (RFC 9147) ← 本文檔
DTLS 1.3直接從TLS 1.3派生,沒有DTLS 1.1這個版本(版本號對齊TLS)。這意味著:
- TLS 1.3的所有握手優(yōu)化(1-RTT、0-RTT)DTLS 1.3都繼承
- TLS 1.3廢棄的(CBC模式、壓縮、重協(xié)商)DTLS 1.3也廢棄
2. 為什么DTLS需要Connection ID?(核心設(shè)計)
2.1 問題的根源:NAT + UDP = 地址會變的連接
這是DTLS獨有的核心需求,TLS完全不需要這東西。為什么?
TCP的場景:
Client:192.168.1.10:54321 ──TCP──> Server:10.0.0.1:443
↑
這個五元組(5-tuple)在整個連接生命周期不變
NAT表項保持,TCP連接維持
UDP的場景:
階段1:
Client:192.168.1.10:54321 ──UDP──> Server:10.0.0.1:443
(NAT映射為 203.0.113.5:60001)
階段2(NAT超時后,或者客戶端切WiFi到4G):
Client:192.168.1.10:40000 ──UDP──> Server:10.0.0.1:443 ← 源端口變了!
(NAT映射為 203.0.113.5:60002)
問題:Server收到階段2的包,5-tuple變了,怎么知道這是同一個DTLS連接?
原文:
"DTLS records without CIDs do not contain any association identifiers, and applications must arrange to multiplex between associations. With UDP, the host/port number is used to look up the appropriate security association for incoming records without CIDs."
沒有CID時的查找方式:
收到UDP包(src_ip, src_port, dst_ip, dst_port)
↓
查哈希表(5-tuple → DTLS Association)
↓
問題:NAT重新映射后5-tuple變了,找不到關(guān)聯(lián)!
2.2 Connection ID的設(shè)計:在記錄層嵌入連接標(biāo)識
CID的本質(zhì):DTLS協(xié)議在每個記錄頭部嵌入一個連接標(biāo)識符,接收方用CID(而不是IP+端口)來查找對應(yīng)的DTLS連接。
DTLS 1.3記錄頭部(含CID):
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|0|0|1|1|1|1|E E| ← C bit = 1 (CID Present)
+-+-+-+-+-+-+-+-+
| Connection | ← CID (協(xié)商好的長度)
| ID |
/ (negotiated) /
| (e.g. 4B) |
+-+-+-+-+-+-+-+-+
| 16-bit |
| Seq Number |
+-+-+-+-+-+-+-+-+
| Length |
+-+-+-+-+-+-+-+-+
查找方式變了:
收到UDP包 → 解析DTLS頭部 → 提取CID → 查哈希表(CID → Association)
↑
CID不變,即使IP和端口全變了!
2.3 CID的生命周期
握手協(xié)商CID 數(shù)據(jù)傳輸 CID更新
Client ─────────────> Server Client ──────> Server Client <──────> Server
| | | | | |
| ClientHello | | CID=5 | | [KeyUpdate] |
| + connection_id=5 | |─────────────>| | [NewCID] |
| ("我用CID=5標(biāo)識 | | | | |
| 發(fā)給你們的包") | | CID=100 | | |
| | |<─────────────| | |
|<──────── ServerHello | | ("Server用 | | |
| + connection_id=100 | | CID=100" ) | | |
| ("你們發(fā)給我們的 | | | | |
| 包請帶CID=100") | | | | |
每個方向獨立:Client→Server的CID 和 Server→Client的CID 是不同的,各自獨立協(xié)商。
這是DTLS獨有的核心機制——TLS運行在TCP上,TCP連接就是標(biāo)識,不需要額外的東西。DTLS運行在UDP上,UDP沒有"連接"概念,所以必須自己造一個標(biāo)識嵌入?yún)f(xié)議里。
2.4 CID更新的實現(xiàn)要點
// RFC中的數(shù)據(jù)結(jié)構(gòu)
struct {
ConnectionId cids<0..2^16-1>; // 一批新的CID
ConnectionIdUsage usage; // cid_immediate(0) 或 cid_spare(1)
} NewConnectionId;
// 兩種模式:
// cid_immediate: 收到后立即切換使用新CID
// cid_spare: 先留著備用(比如多路徑場景,每條路徑用一個CID)
約束:
- 同一時刻只能有一個未確認(rèn)的NewConnectionId請求(MUST NOT have more than one outstanding)
- 收到RequestConnectionId后應(yīng)盡快響應(yīng)
- 換路徑時應(yīng)該使用新CID(防追蹤)
2.5 CID的隱私保護意義
"The mechanism for encrypting sequence numbers prevents trivial tracking by on-path adversaries that attempt to correlate the pattern of sequence numbers received on different paths"
如果沒有CID:
- 客戶端從WiFi切換到4G,IP變了,必須重新握手建立新連接
- 攻擊者看到兩個不同IP上出現(xiàn)了類似的DTLS流量模式 → 關(guān)聯(lián)為同一客戶端
有了CID+序列號加密:
- 客戶端可以無縫切換網(wǎng)絡(luò)(連接不斷)
- 不同路徑用不同CID + 序列號加密 → 攻擊者無法關(guān)聯(lián)
3. 記錄層重新設(shè)計:可變長頭部與序列號加密
3.1 頭部格式:為什么DTLS 1.3要重新設(shè)計?
DTLS 1.2的頭部(固定13字節(jié)):
+--------+--------+--------+--------+--------
| Type(1)|Version(2)|Epoch(2)|Seq(6) |Len(2)|
+--------+--------+--------+--------+--------
21/22/23 FEFD 0000 000000000001 xxxx
DTLS 1.3的頭部(可變,最小2字節(jié)?。?/p>
明文記錄 (DTLSPlaintext):
+--------+--------+--------+--------+--------
| Type(1)|Version(2)|Epoch(2)|Seq(6) |Len(2)|
+--------+--------+--------+--------+--------
密文記錄 (DTLSCiphertext) - 最小頭部:
+--------+
|001000EE| ← 僅2字節(jié):固定3位 + 標(biāo)志位 + Epoch低2位
+--------+ ← 后面直接就是加密內(nèi)容(無CID/無長度/8位序列號)
為什么能省這么多? TLS 1.3在設(shè)計時就意識到頭部開銷對物聯(lián)網(wǎng)/低帶寬場景是致命的。DTLS 1.3進(jìn)一步激進(jìn)優(yōu)化。
3.2 Unified Header的位級解析
字節(jié)0: 0 0 1 C S L E1 E0
───── ─ ─ ─ ────
固定 CID 序列號 長度 Epoch低位
標(biāo)志 長度標(biāo)志 標(biāo)志
// C=0,S=0,L=0: 最小頭部,2字節(jié),無CID,8位序列號,無長度
// C=1,S=1,L=1: 最大頭部,含CID,16位序列號,有長度
最小變體 (2字節(jié)):
+--------+--------+
|001000EE|8位Seq |
+--------+--------+
頭部 加密數(shù)據(jù)(占滿UDP包剩余空間)
完整變體 (最大):
+--------+--------+--------+--------+--------+--------+
|0011SLEE| CID | CID | CID | CID |16位Seq |
+--------+--------+--------+--------+--------+--------+
| Seq | Length | 加密數(shù)據(jù)...
+--------+----------+
3.3 序列號加密:為什么這很重要?
攻擊場景(沒有序列號加密時):
攻擊者 Eve 在路徑上監(jiān)聽:
包1: Seq=100, len=100
包2: Seq=101, len=100
包3: Seq=102, len=1500 ← 大流量包
包4: Seq=103, len=100
Eve 分析序列號模式 → 推斷出通信模式
即使CID換了,如果序列號是連續(xù)的,仍可跨路徑追蹤
DTLS 1.3的序列號加密:
加密序列號 = 原始序列號 XOR Mask
其中 Mask 的生成方式:
AES-based: Mask = AES-ECB(sn_key, Ciphertext[0:16])
ChaCha20: Mask = ChaCha20(sn_key, Ciphertext[0:4], Ciphertext[4:16])
關(guān)鍵:Mask 依賴于密文內(nèi)容,每次都不一樣!
→ 線上看不到原始序列號
→ 攻擊者無法進(jìn)行序列號模式分析
實現(xiàn)約束:
- 密文必須至少16字節(jié)(否則無法生成Mask)
- 如果原始數(shù)據(jù)太短,需要padding(TLS_AES_128_CCM_8_SHA256的tag只有8字節(jié),這種情況需要特別注意)
3.4 解復(fù)用邏輯(Demultiplexing):收到UDP包怎么判斷格式?
這是實現(xiàn)時的第一道關(guān)口,必須正確:
收到UDP數(shù)據(jù)包,看第一個字節(jié):
+-------------------+
| 第一個字節(jié) (OCT) |
+-------------------+
|
+--------------+--------------+--------------+
| | | |
OCT==20 OCT==21/22 32<=OCT<64 其他值
| /26\ /001xxxx\ |
↓ ↓ ↓ ↓
ChangeCipherSpec 明文DTLS DTLSCiphertext 拒絕
(DTLS<1.3) (Plaintext) (加密記錄, (deprotection
ACK屬于此類 真正的內(nèi)容在 failed)
解密后的內(nèi)部)
解密后看內(nèi)部ContentType:
DCT==21 → Alert
DCT==22 → Handshake
DCT==23 → Application Data
DCT==26 → ACK
關(guān)鍵區(qū)間:32-63 (0b0010 0000 to 0b0011 1111) 被DTLS 1.3加密記錄占用。IANA不會在這個范圍內(nèi)分配新的Content Type。
4. ACK機制:DTLS 1.3最大的協(xié)議創(chuàng)新
4.1 為什么DTLS 1.2不需要ACK,而DTLS 1.3需要?
DTLS 1.2的重傳策略:
DTLS 1.2: 定時器超時 → 重傳整個Flight的所有消息
問題:
Client發(fā)送Flight(假設(shè)5條消息,1000字節(jié))
↓
只有第3條丟了 → Server無法處理(缺中間消息)
↓
定時器超時 → Client重傳全部5條消息(1000字節(jié))
↓
浪費帶寬!尤其在高丟包網(wǎng)絡(luò)
DTLS 1.3的ACK策略:
Client發(fā)送Flight(5條消息)
↓
Server收到3條,缺第3條 → 發(fā)ACK("我收到0,1,2,4,缺3")
↓
Client收到ACK → 只重傳第3條!
↓
大大節(jié)省帶寬,在高丟包網(wǎng)絡(luò)尤其重要
原文:
"ACK messages are used in two circumstances, namely: On sign of disruption, or lack of progress; and To indicate complete receipt of the last flight in a handshake."
ACK的兩種用途:
- 加速恢復(fù):檢測到接收中斷時,告訴對端"我收到了哪些",讓對端選擇性重傳
- 協(xié)議正確性:確認(rèn)最后一個Flight(Mandatory)——沒有后續(xù)Flight來"隱式確認(rèn)"它了
4.2 ACK的格式
struct {
RecordNumber record_numbers<0..2^16-1>;
} ACK;
RecordNumber結(jié)構(gòu):
struct {
uint64 epoch; // 8字節(jié)
uint64 sequence_number; // 8字節(jié)
} RecordNumber; // 共16字節(jié)
一個ACK消息包含一組已收到記錄的(epoch, seq_num)列表,按序排列。
4.3 ACK的發(fā)送時機
必須發(fā)ACK的情況:
1. 收到亂序消息(不是期望的下一個消息/分片)
→ 立即發(fā)ACK告知"我收到了這些"
2. 收到部分Flight后,短時間內(nèi)沒收到剩余部分
→ 設(shè)置定時器(1/4重傳定時器值),超時發(fā)ACK
3. 收到"最后一個Flight"
→ 必須ACK,否則對端會一直重傳到上限
不需要發(fā)ACK的情況(隱式確認(rèn)):
ClientHello → 不需要ACK(Server會回HelloRetryRequest/ServerHello)
ServerHello Flight → 不需要ACK(Client會回Certificate+Finished)
Client的Cert Flight → 不需要ACK(如果有的話...實際上這是最后一個,需要ACK)
等等,這里有個微妙點??丛模?/p>
"When a handshake flight is sent without any expected response, as is the case with the client's final flight or with the NewSessionTicket message, the flight must be acknowledged with an ACK message."
理解:如果發(fā)了一個Flight,之后"沒有下一個Flight要發(fā)",那這個Flight就必須被ACK。
- 完整握手中,Client的最后一個Flight(Certificate+Finished)→ 需要ACK
- NewSessionTicket → 需要ACK
- KeyUpdate → 需要ACK
4.4 ACK的Epoch約束
重要規(guī)則:
"During the handshake, ACK records MUST be sent with an epoch which is equal to or higher than the record which is being acknowledged."
舉例:
場景:Server的Flight跨越多個epoch
Record 0: ServerHello (epoch 0) ← 你收到了
Record 1: EncryptedExtensions (epoch 2) ← 你也收到了
Record 2: Certificate (epoch 2)
你想ACK Record 0和Record 1:
→ ACK必須用epoch ≥ 2來發(fā)(不能用epoch 0)
→ 因為你已經(jīng)進(jìn)入了epoch 2,你應(yīng)該用當(dāng)前最高epoch發(fā)ACK
4.5 ACK在握手流程中的實際例子
Client Server
ClientHello (Record 0, epoch=0) ──────>
(丟了!)
[定時器超時]
ClientHello (Record 0, epoch=0) ──────>
<────── ServerHello (Record 0, epoch=0)
<────── EncryptedExtensions (Record 1, epoch=2)
<────── Certificate (Record 2, epoch=2)
<────── CertificateVerify (Record 3, epoch=2)
<────── Finished (Record 4, epoch=2)
ACK [] (Record 1, epoch=2) ──────>
↑
注意:這是空ACK!為什么?
因為Client在收到ServerHello后才能推導(dǎo)出epoch 2的密鑰,
才能解密EncryptedExtensions等。收到這些后,它才能ACK。
但此時Server的Flight已經(jīng)完整到達(dá)。
空ACK的作用是:告訴Server"我還在,繼續(xù)",加速Server的響應(yīng)。
Certificate (Record 2, epoch=2) ──────>
CertificateVerify (Record 3, epoch=2) ────>
Finished (Record 4, epoch=2) ──────>
<────── ACK [2,3,4] (Record 5, epoch=3)
↑
Server確認(rèn)收到客戶端最后Flight
4.6 ACK的接收處理
收到ACK時:
1. 遍歷record_numbers列表
2. 對每個已確認(rèn)的記錄,標(biāo)記對應(yīng)消息為"已ACK"
3. 如果Flight中所有消息都已ACK → 取消重傳定時器
4. 如果部分ACK → 只重傳未被確認(rèn)的消息
5. 握手流程與DoS防護(Cookie交換)
5.1 為什么DTLS必須做Cookie交換,TLS不用?
TLS場景(有TCP保護):
攻擊者偽造ClientHello(源IP = 受害者IP)
↓
TCP三次握手 → 受害者不會回ACK/SYN-ACK → 連接建不起來
↓
攻擊失?。═CP本身就是可達(dá)性驗證)
DTLS場景(無連接保護):
攻擊者偽造ClientHello(源IP = 受害者IP,比如8.8.8.8)
↓
Server → 分配狀態(tài) → 做密鑰運算 → 發(fā)送大證書(可能幾KB~幾十KB)
↓
流量全部打到 8.8.8.8(受害者被洪水攻擊?。? ↓
攻擊成功:Server成了攻擊放大器
Cookie交換 = 在DTLS層面實現(xiàn)"可達(dá)性驗證":
ClientHello ────> Server
↓
不分配任何狀態(tài)!
只生成Cookie = HMAC(客戶端IP, ClientHello哈希, 時間戳, 密鑰)
↓
HelloRetryRequest + Cookie ────> Client
↓
Client必須帶回Cookie
↓
ClientHello + Cookie ────> Server
↓
驗證Cookie( Stateless,只需驗HMAC )
驗證通過 → 開始正式握手,分配狀態(tài)
5.2 實現(xiàn)細(xì)節(jié)
原文:
"The HelloRetryRequest is designed to be small enough that it will not itself be fragmented, thus avoiding concerns about interleaving multiple HelloRetryRequests."
關(guān)鍵點:HelloRetryRequest本身不需要重傳機制。為什么?
- 服務(wù)器發(fā)HelloRetryRequest后不創(chuàng)建任何狀態(tài)
- 如果HelloRetryRequest丟了,客戶端超時重傳ClientHello
- 服務(wù)器收到重傳的ClientHello后,再發(fā)一次HelloRetryRequest
- 不需要在服務(wù)器端維護"我發(fā)過HelloRetryRequest給這個客戶端"的狀態(tài)
Cookie的內(nèi)容:
Cookie = HMAC-SHA256(ServerKey, ClientIP || ClientHelloHash || Timestamp)
推薦包含:
- 客戶端IP地址(防止攻擊者偷別人的Cookie來用)
- ClientHello的哈希值(握手日志需要)
- 時間戳(Cookie有效期控制)
- 服務(wù)器密鑰標(biāo)識符(支持密鑰輪換)
5.3 完整握手流程圖
Full DTLS Handshake (with Cookie Exchange)
Client Server
| |
| ClientHello |
| (no cookie) +--------+ |
|-----------------------------> | Flight | |
| +--------+ |
| |
| +--------+ |
|<------------------------ HelloRetryRequest | Flight | |
| + cookie +--------+ |
| |
| ClientHello |
| + cookie +--------+ |
|-----------------------------> | Flight | |
| +--------+ |
| |
| ServerHello |
| {EncryptedExtensions} |
| {CertificateRequest*} |
|<---------+ {Certificate*} |
| | {CertificateVerify*} |
| | {Finished} |
| | +--------+ |
| | | Flight | |
| | +--------+ |
| +-- 這里Server一口氣把所有消息發(fā)過來 |
| |
| {Certificate*} |
| {CertificateVerify*} +--------+ |
| {Finished} + [AppData*] ----------------------> | Flight | |
| +--------+ |
| |
| +--------+ |
|<--------------------------- [ACK] | Flight | |
| [AppData*] +--------+ |
| |
| [AppData] <--------------------------------------------> [AppData]
圖例說明:
{} = 用handshake_traffic_secret保護的記錄 (epoch 2)
[] = 用application_traffic_secret保護的記錄 (epoch 3)
() = 未加密的記錄 (epoch 0)
* = 可選的消息
5.4 PSK/0-RTT握手(無Cookie交換)
PSK Handshake (without Cookie Exchange)
Client Server
| ClientHello |
| + pre_shared_key +--------+ |
| + psk_key_exchange_modes | Flight | |
| + key_share* -----------------------------> +--------+ |
| |
| ServerHello |
| + pre_shared_key |
|<----------------------------- + key_share* |
| {EncryptedExtensions} |
| {Finished} |
| [AppData*] |
| +--------+ |
| | Flight | |
| +--------+ |
| {Finished} +--------+ |
| [AppData*] ---------------------------------------> | Flight | |
| +--------+ |
| +--------+ |
|<-------------------------------- [ACK] | Flight | |
| [AppData*] +--------+ |
| |
| [AppData] <------------------------------------------> [AppData]
注意:0-RTT場景如果跳過Cookie交換,Server發(fā)送的數(shù)據(jù)量不能超過Client發(fā)送量的3倍(防放大)。
5.5 0-RTT握手
Zero-RTT Handshake
Client Server
| ClientHello |
| + early_data +--------+ |
| + psk_key_exchange_modes | Flight | |
| + key_share* +--------+ |
| + pre_shared_key |
| (Application Data*) ---------------------------------> |
| 這里客戶端在第1個包就發(fā)了應(yīng)用數(shù)據(jù)!|
| |
| ServerHello |
| + pre_shared_key |
|<----------------------------- + key_share* |
| {EncryptedExtensions} |
| {Finished} |
| [AppData*] |
| +--------+ |
| | Flight | |
| +--------+ |
| {Finished} +--------+ |
| [AppData*] ---------------------------------------> | Flight | |
| +--------+ |
| +--------+ |
|<-------------------------------- [ACK] | Flight | |
| [AppData*] +--------+ |
| |
| [AppData] <------------------------------------------> [AppData]
6. Epoch與密鑰管理
6.1 Epoch的本質(zhì):密鑰版本號
Epoch = 一個整數(shù)編號,每次密鑰切換時遞增
不同Epoch對應(yīng)不同的密鑰:
Epoch 0: 明文,無加密(ClientHello, ServerHello, HelloRetryRequest)
Epoch 1: client_early_traffic_secret(0-RTT early data)
Epoch 2: [sender]_handshake_traffic_secret(握手加密消息)
Epoch 3: [sender]_application_traffic_secret_0(初始應(yīng)用數(shù)據(jù))
Epoch 4+: [sender]_application_traffic_secret_N(Key Update后的數(shù)據(jù))
為什么需要Epoch? UDP包可能亂序到達(dá),你收到一個加密包時,必須知道用哪把鑰匙解密。Epoch就是鑰匙的編號。
6.2 Epoch在握手過程中的變化
Client Server
| |
| ClientHello (epoch=0) |
| ----------------------------> |
| |
| <-------- HelloRetryRequest (epoch=0)
| |
| ClientHello (epoch=0) |
| ----------------------------> |
| |
| <-------- ServerHello (epoch=0)
| {EncryptedExtensions} (epoch=2)
| {Certificate} (epoch=2)
| {CertificateVerify} (epoch=2)
| {Finished} (epoch=2)
| |
| {Certificate} (epoch=2) |
| {CertificateVerify} (epoch=2) |
| {Finished} (epoch=2) ──────────────> |
| |
| <──────── [ACK] (epoch=3) |
| |
| [Application Data] (epoch=3) |
| <───────────────────────────> [Application Data] (epoch=3)
| |
| Some time later ... |
| |
| <──────── [NewSessionTicket] (epoch=3)
| [ACK] (epoch=3) |
| ────────────────────────────> |
| |
| Some time later ... (Rekeying) |
| |
| <──────── [AppData] (epoch=4) |
| [AppData] (epoch=4) |
| ────────────────────────────> |
6.3 Epoch與記錄號的關(guān)系
完整記錄號 = (epoch, sequence_number) 共16字節(jié)
線上傳輸?shù)? 完整版本
─────────── ───────────
Epoch: 低2位 (EE位) → uint64 (8字節(jié))
Seq Num: 低8/16位 → uint64 (8字節(jié))
Epoch的重構(gòu):線上只有2位Epoch,怎么知道是哪個Epoch?
- 握手階段:Epoch低2位就能唯一確定(只有0和2在用)
- 應(yīng)用階段:找最近的成功解密的epoch,低2位匹配即可
序列號的重構(gòu):
已知的:當(dāng)前epoch最高成功解密序列號 = 500
收到的:線上8位序列號 = 0xF5 (245)
推導(dǎo):完整序列號應(yīng)該是讓低8位=245,且最接近501的值
可能的值:245, 501, 757, 1013...
最接近501的是 501 本身 (0x1F5, 低8位=0xF5)
所以完整序列號 = 501
7. 分片與重組
7.1 為什么握手消息需要分片?
TLS握手消息最大: 2^24 - 1 = 16,777,215 bytes (~16MB)
UDP典型MTU: ~1200-1500 bytes(去掉IP/UDP頭后)
證書鏈很容易達(dá)到幾十KB → 必須分片!
7.2 分片機制
原始握手消息(假設(shè) 3000 bytes):
+--------+--------+----------------------------+
|msg_type| length | body (3000 bytes) |
| 22 | 3000 | |
+--------+--------+----------------------------+
分成3個DTLS分片:
Fragment 1:
+--------+--------+---------+----------+----------+-----------+
|msg_type| length | msg_seq | frag_off | frag_len | body[0:1000]|
| 22 | 3000 | 0 | 0 | 1000 | |
+--------+--------+---------+----------+----------+-----------+
Fragment 2:
+--------+--------+---------+----------+----------+-------------+
|msg_type| length | msg_seq | frag_off | frag_len |body[1000:2000]|
| 22 | 3000 | 0 | 1000 | 1000 | |
+--------+--------+---------+----------+----------+-------------+
Fragment 3:
+--------+--------+---------+----------+----------+-------------+
|msg_type| length | msg_seq | frag_off | frag_len |body[2000:3000]|
| 22 | 3000 | 0 | 2000 | 1000 | |
+--------+--------+---------+----------+----------+-------------+
關(guān)鍵:
- 所有分片的 msg_seq 相同(屬于同一個消息)
- 所有分片的 length 相同(原始消息總長)
- frag_off 遞增,frag_len 是當(dāng)前片大小
7.3 重組實現(xiàn)要點
收到分片時的處理邏輯:
1. 檢查 msg_seq == next_receive_seq?
- 是 → 進(jìn)入重組流程
- 否 → 緩存,等待前面的消息
2. 檢查分片是否已有?
- 用 (frag_off, frag_len) 標(biāo)記已收到的區(qū)域
- 允許重疊?。ㄖ貍鲿r可能改變分片大小)
3. 收集完成后:
- 按 frag_off 排序拼接
- 去掉 DTLS 頭(msg_seq/frag_off/frag_length)
- 還原成標(biāo)準(zhǔn) TLS Handshake 結(jié)構(gòu)
- 加入 transcript 哈希
4. 驗證:
- 所有字節(jié)到齊?
- 長度 == 原始 length?
重要約束:
- 重傳時不能修改消息內(nèi)容的字節(jié)(MUST NOT change handshake message bytes upon retransmission)
- 但可以改變分片大小(允許重疊分片)
- 接收方應(yīng)檢查重傳字節(jié)是否一致,不一致則abort(illegal_parameter alert)
8. 重傳狀態(tài)機:實現(xiàn)層面的核心
8.1 狀態(tài)機
+-----------+
+------->| PREPARING |
| | |
| +-----------+
| |
| | Buffer next flight
| |
| v
| +-----------+
| | |
+-------| SENDING |<------------------+
| | | |
| +-----------+ |
Receive next | | |
flight | | Send flight or partial |
| | flight |
| | Set retransmit timer |
| v |
| +-----------+ |
| | | |
+-------| WAITING |-------------------+
| | | Timer expires |
| +-----------+ |
| | |
| +--------+---------+ |
| | | |
| | Receive record | Receive ACK |
| | (Maybe Send ACK) | for last |
| | | flight |
| v v |
| +-----------+ +-----------+ |
| | Send ACK | | FINISHED | |
| | maybe | | | |
| | retransmit| +-----------+ |
| +-----------+ ^ |
| | |
+-----------------------+----------------+
8.2 狀態(tài)詳解
| 狀態(tài) | 行為 |
|---|---|
| PREPARING | 計算/準(zhǔn)備下一個Flight的消息,放入發(fā)送緩沖區(qū),進(jìn)入SENDING |
| SENDING | 發(fā)送緩沖區(qū)的所有消息(或部分消息),設(shè)置重傳定時器,進(jìn)入WAITING |
| WAITING | 等待對端響應(yīng)。有4種退出方式(見下) |
| FINISHED | 握手完成。但Server仍需在2×MSL內(nèi)響應(yīng)Client最后Flight的重傳 |
8.3 WAITING的4種退出方式
1. 定時器超時
→ 進(jìn)入SENDING重傳整個Flight
→ 定時器翻倍(指數(shù)退避)
→ 回到WAITING
2. 收到ACK(部分確認(rèn))
→ 進(jìn)入SENDING,只重傳未被確認(rèn)的消息
→ 重置定時器
→ 回到WAITING
3. 收到對端的重傳Flight
(說明你之前發(fā)的Flight對方?jīng)]收到)
→ 進(jìn)入SENDING重傳你的Flight
→ 重置定時器
→ 回到WAITING
4. 收到下一個Flight
→ 如果是最后Flight → FINISHED
→ 否則 → PREPARING(準(zhǔn)備你的下一個Flight)
8.4 定時器配置
默認(rèn)配置:
- 初始超時 = 1000ms
- 每次重傳翻倍
- 上限 = 60秒
有RTT信息時:
- 超時 = 1.5 × RTT
實測到RTT后:
- 超時 = 1.5 × 實測RTT
長時間空閑后(> 10 × 當(dāng)前超時):
- 重置為初始值
特殊場景:
- DTLS-SRTP(實時語音):400ms
- 物聯(lián)網(wǎng)低功耗mesh:可能更長
8.5 Post-Handshake消息的狀態(tài)機
每種post-handshake消息類型有獨立的狀態(tài)機:
允許批量發(fā)(不等ACK)的:
- NewSessionTicket: 可以一次發(fā)多個
- CertificateRequest: 可以一次發(fā)多個
必須等ACK后才能發(fā)下一個的:
- KeyUpdate
- NewConnectionId
- RequestConnectionId
為什么KeyUpdate必須等ACK?
如果不等ACK:
Client發(fā)KeyUpdate (epoch=4)
↓
Client緊接著發(fā)另一個KeyUpdate (epoch=5)
↓
第一個KeyUpdate的ACK丟了
↓
Server收到epoch=5的包,但沒見過epoch=4的KeyUpdate
↓
Server不知道怎么辦 → 協(xié)議失敗
所以約束:MUST NOT send records with the new keys until the previous KeyUpdate has been acknowledged
9. Key Update與Connection ID更新
9.1 Key Update流程
Client Server
| |
| [AppData] (epoch=3) |
| ────────────────────────────> |
| |
| <──────── [AppData] (epoch=3) |
| |
| [KeyUpdate] (+update_requested) |
| (epoch=3) ────────────────────────────> |
| |
| <──────── [AppData] (epoch=3) |
| |
| <──────── [ACK] (epoch=3) |
| ← 收到ACK后才能切換密鑰 |
| |
| [AppData] (epoch=4) ← 用新密鑰! |
| ────────────────────────────> |
| |
| <──────── [KeyUpdate] |
| (epoch=3) |
| [ACK] (epoch=4) |
| ────────────────────────────> |
| |
| <──────── [AppData] (epoch=4) |
9.2 Connection ID更新流程
Client Server
| |
| [NewSessionTicket] (epoch=3) |
| <──────────────────────────── |
| |
| [ACK] (epoch=3) |
| ────────────────────────────> |
| |
| 一段時間后... |
| |
| <──────── [NewConnectionId] |
| cids=[10,11,12] |
| usage=cid_spare |
| |
| [ACK] (epoch=3) |
| ────────────────────────────> |
| |
| Client切換到新路徑(WiFi → 4G) |
| IP變了,開始用 CID=10 發(fā)數(shù)據(jù) |
| [AppData] (cid=10, epoch=3) |
| ────────────────────────────> |
| |
| Server看到新IP但CID=10 → 查到是同一個連接 |
| <──────── [AppData] (cid=5) |
| (Server的CID沒變) |
10. AEAD安全邊界與實現(xiàn)約束
10.1 為什么DTLS的AEAD限制比TLS更嚴(yán)格?
TLS 1.3行為:
收到認(rèn)證失敗的包 → 立即斷開連接
→ 攻擊者只有一次嘗試機會
DTLS行為:
收到認(rèn)證失敗的包 → 靜默丟棄
→ 攻擊者可以不斷嘗試偽造!
→ 必須計數(shù)丟棄次數(shù),超過上限斷開
10.2 各AEAD算法的安全邊界
| AEAD算法 | 加密包上限 | 認(rèn)證失敗上限 | 備注 |
|---|---|---|---|
| AES-128-GCM | 見TLS 1.3 | 2^36 | |
| AES-256-GCM | 見TLS 1.3 | 2^36 | |
| ChaCha20-Poly1305 | 見TLS 1.3 | 2^36 | |
| AES-128-CCM | 2^23 | 2^23.5 | 更嚴(yán)格 |
| AES-128-CCM-8 | 自定義 | 自定義 | 禁止無額外防護使用 |
10.3 實現(xiàn)要求
// 每個epoch必須維護的計數(shù)器
struct EpochState {
uint64_t encrypt_count; // 成功加密的包數(shù)
uint64_t auth_failure_count; // 認(rèn)證失敗的包數(shù)
// 達(dá)到任一上限時必須:
// 1. 發(fā)起 Key Update
// 2. 或關(guān)閉連接
};
11. 實現(xiàn)陷阱與踩坑指南
11.1 Epoch管理
坑1:收到舊Epoch的包
場景:
Server發(fā)送Finished (epoch 2)后進(jìn)入epoch 3
但網(wǎng)絡(luò)亂序,Client的某個epoch 2的包晚到了
正確做法:
保留舊epoch的密鑰至少2×MSL(~4分鐘)
嘗試用舊密鑰解密
成功后處理,但不要用這個包更新滑動窗口
坑2:Epoch回繞
Epoch是uint64_t,理論上不可能回繞
但必須顯式檢查:達(dá)到 2^48-1 后禁止繼續(xù)KeyUpdate
→ 必須終止連接建新連接
11.2 序列號重構(gòu)
坑:收到亂序包時怎么重構(gòu)完整序列號?
已收到的:epoch=3, seq=100, 101, 102, 105
現(xiàn)在收到:線上8位序列號 = 0x67 (103)
最近的完整seq = 105
期望的下一個 = 106
候選值:
低8位=0x67的可能值:...55, 311, 567, 823...
最接近106的是 103 (0x67)
所以完整序列號 = 103
但注意:如果網(wǎng)絡(luò)大幅亂序,這個算法可能選錯!
(不過選錯的結(jié)果只是解密失敗,不會有安全問題)
11.3 ACK處理的邊界情況
坑1:空ACK
場景:收到EncryptedExtensions但還沒收到ServerHello
問題:無法解密EncryptedExtensions,不敢確認(rèn)它
解決:發(fā)空ACK(record_numbers為空)
作用:告訴對端"我還活著,繼續(xù)發(fā)"
坑2:收到ACK時Flight已被部分確認(rèn)
必須只重傳未確認(rèn)的消息
需要維護每條消息→所在記錄的映射
坑3:握手階段收到epoch比ACK目標(biāo)低的記錄
規(guī)則:ACK必須用 ≥ 被確認(rèn)記錄的epoch來發(fā)
實現(xiàn):始終用當(dāng)前最高發(fā)送epoch來發(fā)ACK
11.4 兼容性
坑1:DTLS 1.3不使用TLS 1.3的"compatibility mode"
- Server MUST NOT echo legacy_session_id
- 兩端 MUST NOT 發(fā)送 ChangeCipherSpec
坑2:legacy_version字段
- 所有記錄必須設(shè)為 {254, 253}(即DTLS 1.2的版本號)
- 僅在初始ClientHello中可為 {254, 255}(兼容性)
- 這個字段必須被接收方忽略!
坑3:DTLS 1.3的transcript不包含DTLS特有字段
- message_seq, fragment_offset, fragment_length 不計入哈希
- 這與DTLS 1.2不同!
11.5 多路徑與CID
坑:收到不同源地址的包
DTLS 1.3允許地址變化(通過CID)
但實現(xiàn) MUST NOT 因為收到新地址的包就改變發(fā)送目標(biāo)
為什么?攻擊者可以偽造源地址讓你把流量發(fā)到第三方!
(反射攻擊)
正確做法:
- 有CID的包:用CID查連接,不改變發(fā)送地址
- 沒有CID的包:5-tuple查連接
- 地址更新需要專門的可達(dá)性驗證(本規(guī)范未定義)
附錄:核心數(shù)據(jù)結(jié)構(gòu)與常量速查
記錄層結(jié)構(gòu)
// 明文記錄(握手早期)
struct DTLSPlaintext {
uint8_t type; // ContentType
uint16_t legacy_record_version; // {254, 253}
uint16_t epoch; // 低2字節(jié)
uint8_t sequence_number[6]; // 48位序列號
uint16_t length;
uint8_t fragment[];
};
// 密文記錄頭部(可變長)
// 字節(jié)0: 0 0 1 C S L E1 E0
// 可選: Connection ID (協(xié)商長度)
// 可選: 8位或16位序列號
// 可選: 16位長度
// 解密后的內(nèi)部結(jié)構(gòu)
struct DTLSInnerPlaintext {
uint8_t content[];
uint8_t type; // 真正的ContentType
uint8_t zeros[]; // 填充
};
// 完整記錄號(用于ACK和AEAD)
struct RecordNumber {
uint64_t epoch;
uint64_t sequence_number;
};
握手結(jié)構(gòu)
struct DTLSHandshake {
uint8_t msg_type; // HandshakeType
uint24_t length; // 消息總長度
uint16_t message_seq; // DTLS特有
uint24_t fragment_offset; // DTLS特有
uint24_t fragment_length; // DTLS特有
// body...
};
// HandshakeType
enum {
client_hello(1),
server_hello(2),
new_session_ticket(4),
end_of_early_data(5),
encrypted_extensions(8),
request_connection_id(9), // DTLS 1.3新增
new_connection_id(10), // DTLS 1.3新增
certificate(11),
certificate_request(13),
certificate_verify(15),
finished(20),
key_update(24),
message_hash(254)
};
CID相關(guān)
enum {
cid_immediate(0),
cid_spare(1)
} ConnectionIdUsage;
struct NewConnectionId {
ConnectionId cids[]; // 提供的CID列表
ConnectionIdUsage usage;
};
struct RequestConnectionId {
uint8_t num_cids; // 請求的CID數(shù)量
};
關(guān)鍵常量
| 常量 | 值 | 說明 |
|---|---|---|
| Initial Timer | 1000 ms | 默認(rèn)初始重傳超時 |
| Max Timer | 60 s | 最大重傳超時 |
| Max Epoch | 2^48 - 1 | Epoch上限 |
| AES-128-CCM Limit | 2^23 | 該算法的包上限 |
| Auth Failure Limit (GCM/ChaCha) | 2^36 | GCM/ChaCha認(rèn)證失敗上限 |
| ContentType ACK | 26 | ACK內(nèi)容類型編號 |
| Alert too_many_cids_requested | 52 | 過多CID請求alert |
總結(jié):DTLS 1.3設(shè)計的5個核心思想
- 最少修改原則:在TLS 1.3上做最小必要的UDP適配,不重復(fù)發(fā)明
- 頭部極致壓縮:可變長Unified Header,最小2字節(jié),適應(yīng)物聯(lián)網(wǎng)
- 序列號加密: borrowed from QUIC,防流量分析和跨路徑追蹤
- ACK選擇性重傳:解決DTLS 1.2全Flight重傳的帶寬浪費
- Connection ID:解決NAT/網(wǎng)絡(luò)切換問題,這是TLS完全沒有的需求
推薦閱讀順序:
- 先通讀RFC 9147的Section 3(設(shè)計原理)
- 重點研究Section 4(記錄層格式)和Section 7(ACK)
- 結(jié)合本文檔的實現(xiàn)要點進(jìn)行代碼設(shè)計
- 最后參考Appendix C(實現(xiàn)陷阱)