@@ -72,6 +72,18 @@ impl Dtls13Record {
7272 let ( input, content_type) = ContentType :: parse ( input) ?; // u8
7373 let ( input, version) = ProtocolVersion :: parse ( input) ?; // u16
7474
75+ // RFC 9147 §4.1: Only alert(21), handshake(22), and ack(26) are valid
76+ // plaintext content types in DTLS 1.3. Reject all others.
77+ match content_type {
78+ ContentType :: Alert | ContentType :: Handshake | ContentType :: Ack => { }
79+ _ => {
80+ return Err ( Err :: Failure ( nom:: error:: Error :: new (
81+ input,
82+ nom:: error:: ErrorKind :: Tag ,
83+ ) ) ) ;
84+ }
85+ }
86+
7587 // Accept DTLS 1.0 or 1.2 in record layer per RFC 9147 §5.1
7688 // (same legacy version handling as DTLS 1.2)
7789 match version {
@@ -85,6 +97,16 @@ impl Dtls13Record {
8597 }
8698
8799 let ( input, epoch) = be_u16 ( input) ?; // u16
100+
101+ // RFC 9147 §4.1: DTLSPlaintext records must use epoch 0.
102+ // Epoch values other than 0 in plaintext format are invalid.
103+ if epoch != 0 {
104+ return Err ( Err :: Failure ( nom:: error:: Error :: new (
105+ input,
106+ nom:: error:: ErrorKind :: Tag ,
107+ ) ) ) ;
108+ }
109+
88110 let ( input, sequence_number) = be_u48 ( input) ?; // u48
89111 let ( input, length) = be_u16 ( input) ?; // u16
90112 let ( rest, fragment_slice) = take ( length as usize ) ( input) ?;
@@ -222,3 +244,111 @@ impl fmt::Debug for Dtls13Record {
222244 . finish ( )
223245 }
224246}
247+
248+ #[ cfg( test) ]
249+ mod tests {
250+ use super :: * ;
251+
252+ #[ test]
253+ fn plaintext_content_type_whitelist ( ) {
254+ // RFC 9147 §4.1: Only alert(21), handshake(22), and ack(26) are valid
255+ // plaintext content types in DTLS 1.3. Plaintext records must use epoch 0.
256+ // ApplicationData and other invalid types are rejected at parse time.
257+
258+ fn build_plaintext_record ( content_type : u8 ) -> Vec < u8 > {
259+ vec ! [
260+ content_type, // ContentType
261+ 0xFE ,
262+ 0xFD , // version: DTLS 1.2
263+ 0x00 ,
264+ 0x00 , // epoch: 0 (plaintext)
265+ 0x00 ,
266+ 0x00 ,
267+ 0x00 ,
268+ 0x00 ,
269+ 0x00 ,
270+ 0x01 , // sequence_number
271+ 0x00 ,
272+ 0x02 , // length: 2
273+ 0xAA ,
274+ 0xBB , // fragment payload
275+ ]
276+ }
277+
278+ fn build_plaintext_record_with_epoch ( content_type : u8 , epoch : u16 ) -> Vec < u8 > {
279+ vec ! [
280+ content_type, // ContentType
281+ 0xFE ,
282+ 0xFD , // version: DTLS 1.2
283+ ( epoch >> 8 ) as u8 ,
284+ ( epoch & 0xFF ) as u8 , // epoch
285+ 0x00 ,
286+ 0x00 ,
287+ 0x00 ,
288+ 0x00 ,
289+ 0x00 ,
290+ 0x01 , // sequence_number
291+ 0x00 ,
292+ 0x02 , // length: 2
293+ 0xAA ,
294+ 0xBB , // fragment payload
295+ ]
296+ }
297+
298+ // ALLOWED: Alert (21) with epoch 0
299+ let alert = build_plaintext_record ( 0x15 ) ;
300+ assert ! (
301+ Dtls13Record :: parse( & alert, 0 ) . is_ok( ) ,
302+ "Alert should be allowed in plaintext DTLS 1.3"
303+ ) ;
304+
305+ // ALLOWED: Handshake (22) with epoch 0
306+ let handshake = build_plaintext_record ( 0x16 ) ;
307+ assert ! (
308+ Dtls13Record :: parse( & handshake, 0 ) . is_ok( ) ,
309+ "Handshake should be allowed in plaintext DTLS 1.3"
310+ ) ;
311+
312+ // ALLOWED: Ack (26) with epoch 0
313+ let ack = build_plaintext_record ( 0x1A ) ;
314+ assert ! (
315+ Dtls13Record :: parse( & ack, 0 ) . is_ok( ) ,
316+ "Ack should be allowed in plaintext DTLS 1.3"
317+ ) ;
318+
319+ // REJECTED: ApplicationData (23)
320+ let app_data = build_plaintext_record ( 0x17 ) ;
321+ assert ! (
322+ Dtls13Record :: parse( & app_data, 0 ) . is_err( ) ,
323+ "ApplicationData must be rejected in plaintext DTLS 1.3"
324+ ) ;
325+
326+ // REJECTED: ChangeCipherSpec (20) - valid in DTLS 1.2 but not DTLS 1.3
327+ let ccs = build_plaintext_record ( 0x14 ) ;
328+ assert ! (
329+ Dtls13Record :: parse( & ccs, 0 ) . is_err( ) ,
330+ "ChangeCipherSpec must be rejected in DTLS 1.3"
331+ ) ;
332+
333+ // REJECTED: Unknown ContentType (0xFF)
334+ let unknown = build_plaintext_record ( 0xFF ) ;
335+ assert ! (
336+ Dtls13Record :: parse( & unknown, 0 ) . is_err( ) ,
337+ "Unknown ContentType must be rejected in plaintext DTLS 1.3"
338+ ) ;
339+
340+ // REJECTED: Plaintext format with epoch 1 (invalid per RFC 9147 §4.1)
341+ let epoch_1_handshake = build_plaintext_record_with_epoch ( 0x16 , 1 ) ;
342+ assert ! (
343+ Dtls13Record :: parse( & epoch_1_handshake, 0 ) . is_err( ) ,
344+ "Plaintext format with epoch 1 must be rejected"
345+ ) ;
346+
347+ // REJECTED: Plaintext format with epoch 2 (should use unified header)
348+ let epoch_2_handshake = build_plaintext_record_with_epoch ( 0x16 , 2 ) ;
349+ assert ! (
350+ Dtls13Record :: parse( & epoch_2_handshake, 0 ) . is_err( ) ,
351+ "Plaintext format with epoch 2 must be rejected"
352+ ) ;
353+ }
354+ }
0 commit comments