Skip to main content

brynja_protocol/tls/
dtls.rs

1//! DTLS datagram record framing.
2
3use brynja_core::{ProtocolFamily, ProtocolVersion, ReadCursor, WriteCursor};
4
5use super::{
6    ContentType, ContentTypeCode, LegacyRecordVersion, MAX_PLAINTEXT_LENGTH,
7    MAX_TLS13_CIPHERTEXT_LENGTH, RecordError, WirePolicy,
8};
9
10const PLAINTEXT_HEADER_LENGTH: usize = 13;
11const UNIFIED_FIXED_BITS: u8 = 0x20;
12const UNIFIED_FIXED_MASK: u8 = 0xe0;
13const CID_BIT: u8 = 0x10;
14const LONG_SEQUENCE_BIT: u8 = 0x08;
15const LENGTH_BIT: u8 = 0x04;
16
17/// One borrowed unprotected DTLS record.
18#[derive(Clone, Copy, Eq, PartialEq)]
19pub struct DtlsPlaintext<'input> {
20    content_type: ContentType,
21    legacy_record_version: LegacyRecordVersion,
22    epoch: u16,
23    sequence_number: [u8; 6],
24    fragment: &'input [u8],
25}
26
27/// Expected DTLS 1.3 Connection ID layout for one selected association.
28#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29pub struct Dtls13CiphertextConfig {
30    connection_id_length: u8,
31}
32
33/// The encoded truncated DTLS 1.3 record sequence number.
34#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
35pub enum Dtls13Sequence {
36    /// An eight-bit truncated sequence number.
37    Short(u8),
38    /// A sixteen-bit truncated sequence number.
39    Long(u16),
40}
41
42/// Caller-supplied DTLS 1.3 unified-header fields for encoding.
43#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44pub struct Dtls13CiphertextHeader<'cid> {
45    epoch_bits: u8,
46    connection_id: &'cid [u8],
47    sequence: Dtls13Sequence,
48    length_present: bool,
49}
50
51/// One borrowed protected DTLS 1.3 record with its exact header bytes.
52#[derive(Clone, Copy, Eq, PartialEq)]
53pub struct Dtls13Ciphertext<'input> {
54    unified_header: &'input [u8],
55    connection_id: &'input [u8],
56    sequence: Dtls13Sequence,
57    epoch_bits: u8,
58    length_present: bool,
59    encrypted_record: &'input [u8],
60}
61
62impl<'input> DtlsPlaintext<'input> {
63    /// Constructs a checked DTLS plaintext envelope for encoding.
64    pub fn new(
65        policy: WirePolicy,
66        content_type: ContentTypeCode,
67        legacy_record_version: LegacyRecordVersion,
68        epoch: u16,
69        sequence_number: [u8; 6],
70        fragment: &'input [u8],
71    ) -> Result<Self, RecordError> {
72        require_dtls(policy)?;
73        let content_type = policy.admit_plaintext(content_type)?;
74        if matches!(policy.version(), ProtocolVersion::Dtls13) && epoch != 0 {
75            return Err(RecordError::InvalidPlaintextEpoch);
76        }
77        if matches!(policy.version(), ProtocolVersion::Dtls13)
78            && !matches!(legacy_record_version.bytes(), [254, 253] | [254, 255])
79        {
80            return Err(RecordError::InvalidPlaintextVersion);
81        }
82        validate_plaintext_length(content_type, fragment.len())?;
83        Ok(Self {
84            content_type,
85            legacy_record_version,
86            epoch,
87            sequence_number,
88            fragment,
89        })
90    }
91
92    /// Parses one DTLS 1.2 or DTLS 1.3 plaintext record.
93    pub fn parse(
94        policy: WirePolicy,
95        input: &'input [u8],
96    ) -> Result<(Self, &'input [u8]), RecordError> {
97        require_dtls(policy)?;
98        let mut cursor = ReadCursor::new(input);
99        let code = read_byte(&mut cursor)?;
100        let content_type = policy.admit_plaintext(ContentTypeCode::classify(code))?;
101        let version = read_version(&mut cursor)?;
102        let epoch = read_u16(&mut cursor)?;
103        if matches!(policy.version(), ProtocolVersion::Dtls13) && epoch != 0 {
104            return Err(RecordError::InvalidPlaintextEpoch);
105        }
106        let sequence_number = *cursor
107            .take_array::<6>()
108            .map_err(|_| RecordError::Truncated)?;
109        let length = usize::from(read_u16(&mut cursor)?);
110        validate_plaintext_length(content_type, length)?;
111        let fragment = cursor.take(length).map_err(|_| RecordError::Truncated)?;
112        let remaining = cursor.remaining();
113        Ok((
114            Self {
115                content_type,
116                legacy_record_version: version,
117                epoch,
118                sequence_number,
119                fragment,
120            },
121            remaining,
122        ))
123    }
124
125    /// Returns the admitted content type.
126    #[must_use]
127    pub const fn content_type(&self) -> ContentType {
128        self.content_type
129    }
130
131    /// Returns preserved non-negotiating legacy-version bytes.
132    #[must_use]
133    pub const fn legacy_record_version(&self) -> LegacyRecordVersion {
134        self.legacy_record_version
135    }
136
137    /// Returns the exact encoded epoch.
138    #[must_use]
139    pub const fn epoch(&self) -> u16 {
140        self.epoch
141    }
142
143    /// Returns the exact encoded 48-bit sequence number.
144    #[must_use]
145    pub const fn sequence_number(&self) -> [u8; 6] {
146        self.sequence_number
147    }
148
149    /// Returns the exact borrowed fragment.
150    #[must_use]
151    pub const fn fragment(&self) -> &'input [u8] {
152        self.fragment
153    }
154
155    /// Writes the complete parsed record transactionally.
156    pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
157        let total = PLAINTEXT_HEADER_LENGTH
158            .checked_add(self.fragment.len())
159            .ok_or(RecordError::LengthOverflow)?;
160        if output.len() < total {
161            return Err(RecordError::InsufficientOutput);
162        }
163        let length = u16::try_from(self.fragment.len())
164            .map_err(|_| RecordError::RecordOverflow)?
165            .to_be_bytes();
166        let type_bytes = [self.content_type.code()];
167        let version = self.legacy_record_version.bytes();
168        let epoch = self.epoch.to_be_bytes();
169        let mut cursor = WriteCursor::new(output);
170        cursor
171            .write_parts(&[
172                &type_bytes,
173                &version,
174                &epoch,
175                &self.sequence_number,
176                &length,
177                self.fragment,
178            ])
179            .map_err(|_| RecordError::InsufficientOutput)?;
180        Ok(total)
181    }
182}
183
184impl Dtls13CiphertextConfig {
185    /// Creates an exact selected-association CID layout.
186    pub fn new(connection_id_length: usize) -> Result<Self, RecordError> {
187        let connection_id_length =
188            u8::try_from(connection_id_length).map_err(|_| RecordError::ConnectionIdTooLong)?;
189        Ok(Self {
190            connection_id_length,
191        })
192    }
193
194    /// Returns the exact negotiated CID byte length.
195    #[must_use]
196    pub const fn connection_id_length(self) -> usize {
197        self.connection_id_length as usize
198    }
199}
200
201impl<'cid> Dtls13CiphertextHeader<'cid> {
202    /// Creates checked caller-supplied unified-header fields.
203    pub fn new(
204        epoch_bits: u8,
205        connection_id: &'cid [u8],
206        sequence: Dtls13Sequence,
207        length_present: bool,
208    ) -> Result<Self, RecordError> {
209        if epoch_bits > 3 {
210            return Err(RecordError::InvalidUnifiedHeader);
211        }
212        let _ = u8::try_from(connection_id.len()).map_err(|_| RecordError::ConnectionIdTooLong)?;
213        Ok(Self {
214            epoch_bits,
215            connection_id,
216            sequence,
217            length_present,
218        })
219    }
220
221    /// Returns the low two encoded epoch bits.
222    #[must_use]
223    pub const fn epoch_bits(self) -> u8 {
224        self.epoch_bits
225    }
226
227    /// Returns the exact borrowed Connection ID.
228    #[must_use]
229    pub const fn connection_id(self) -> &'cid [u8] {
230        self.connection_id
231    }
232
233    /// Returns the exact truncated sequence number.
234    #[must_use]
235    pub const fn sequence(self) -> Dtls13Sequence {
236        self.sequence
237    }
238
239    /// Reports whether the encoded header includes a length field.
240    #[must_use]
241    pub const fn length_present(self) -> bool {
242        self.length_present
243    }
244}
245
246impl<'input> Dtls13Ciphertext<'input> {
247    /// Parses one protected DTLS 1.3 record under an exact CID context.
248    ///
249    /// A record without a length field consumes the rest of the datagram.
250    pub fn parse(
251        policy: WirePolicy,
252        config: Dtls13CiphertextConfig,
253        input: &'input [u8],
254    ) -> Result<(Self, &'input [u8]), RecordError> {
255        if !matches!(policy.version(), ProtocolVersion::Dtls13) {
256            return Err(RecordError::ProfileMismatch);
257        }
258        let mut cursor = ReadCursor::new(input);
259        let first = read_byte(&mut cursor)?;
260        if first & UNIFIED_FIXED_MASK != UNIFIED_FIXED_BITS {
261            return Err(RecordError::InvalidUnifiedHeader);
262        }
263        let cid_present = first & CID_BIT != 0;
264        if cid_present != (config.connection_id_length != 0) {
265            return Err(RecordError::ConnectionIdMismatch);
266        }
267        let connection_id = cursor
268            .take(config.connection_id_length())
269            .map_err(|_| RecordError::Truncated)?;
270        let sequence = if first & LONG_SEQUENCE_BIT == 0 {
271            Dtls13Sequence::Short(read_byte(&mut cursor)?)
272        } else {
273            Dtls13Sequence::Long(read_u16(&mut cursor)?)
274        };
275        let length_present = first & LENGTH_BIT != 0;
276        let encrypted_length = if length_present {
277            usize::from(read_u16(&mut cursor)?)
278        } else {
279            cursor.remaining_len()
280        };
281        validate_ciphertext_length(encrypted_length)?;
282        let encrypted_record = cursor
283            .take(encrypted_length)
284            .map_err(|_| RecordError::Truncated)?;
285        let remaining = cursor.remaining();
286        let header_length = input
287            .len()
288            .checked_sub(encrypted_record.len())
289            .and_then(|length| length.checked_sub(remaining.len()))
290            .ok_or(RecordError::LengthOverflow)?;
291        let unified_header = input.get(..header_length).ok_or(RecordError::Truncated)?;
292        Ok((
293            Self {
294                unified_header,
295                connection_id,
296                sequence,
297                epoch_bits: first & 3,
298                length_present,
299                encrypted_record,
300            },
301            remaining,
302        ))
303    }
304
305    /// Returns the exact encoded unified header.
306    #[must_use]
307    pub const fn unified_header(&self) -> &'input [u8] {
308        self.unified_header
309    }
310
311    /// Returns the exact borrowed Connection ID.
312    #[must_use]
313    pub const fn connection_id(&self) -> &'input [u8] {
314        self.connection_id
315    }
316
317    /// Returns the encoded truncated sequence number.
318    #[must_use]
319    pub const fn sequence(&self) -> Dtls13Sequence {
320        self.sequence
321    }
322
323    /// Returns the low two encoded epoch bits.
324    #[must_use]
325    pub const fn epoch_bits(&self) -> u8 {
326        self.epoch_bits
327    }
328
329    /// Reports whether the wire header contained an explicit length.
330    #[must_use]
331    pub const fn length_present(&self) -> bool {
332        self.length_present
333    }
334
335    /// Returns the exact borrowed protected fragment.
336    #[must_use]
337    pub const fn encrypted_record(&self) -> &'input [u8] {
338        self.encrypted_record
339    }
340
341    /// Writes the complete parsed record transactionally.
342    pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
343        let total = self
344            .unified_header
345            .len()
346            .checked_add(self.encrypted_record.len())
347            .ok_or(RecordError::LengthOverflow)?;
348        if output.len() < total {
349            return Err(RecordError::InsufficientOutput);
350        }
351        let mut cursor = WriteCursor::new(output);
352        cursor
353            .write_parts(&[self.unified_header, self.encrypted_record])
354            .map_err(|_| RecordError::InsufficientOutput)?;
355        Ok(total)
356    }
357}
358
359/// Encodes one checked DTLS 1.3 unified-header record.
360pub fn encode_dtls13_ciphertext(
361    header: Dtls13CiphertextHeader<'_>,
362    encrypted_record: &[u8],
363    output: &mut [u8],
364) -> Result<usize, RecordError> {
365    validate_ciphertext_length(encrypted_record.len())?;
366    let sequence_length = match header.sequence {
367        Dtls13Sequence::Short(_) => 1_usize,
368        Dtls13Sequence::Long(_) => 2_usize,
369    };
370    let length_length = if header.length_present {
371        2_usize
372    } else {
373        0_usize
374    };
375    let total = 1_usize
376        .checked_add(header.connection_id.len())
377        .and_then(|value| value.checked_add(sequence_length))
378        .and_then(|value| value.checked_add(length_length))
379        .and_then(|value| value.checked_add(encrypted_record.len()))
380        .ok_or(RecordError::LengthOverflow)?;
381    if output.len() < total {
382        return Err(RecordError::InsufficientOutput);
383    }
384    let mut first = UNIFIED_FIXED_BITS | header.epoch_bits;
385    if !header.connection_id.is_empty() {
386        first |= CID_BIT;
387    }
388    if matches!(header.sequence, Dtls13Sequence::Long(_)) {
389        first |= LONG_SEQUENCE_BIT;
390    }
391    if header.length_present {
392        first |= LENGTH_BIT;
393    }
394    let first_bytes = [first];
395    let sequence_bytes = match header.sequence {
396        Dtls13Sequence::Short(value) => [0, value],
397        Dtls13Sequence::Long(value) => value.to_be_bytes(),
398    };
399    let sequence = if matches!(header.sequence, Dtls13Sequence::Short(_)) {
400        sequence_bytes.get(1..).ok_or(RecordError::LengthOverflow)?
401    } else {
402        sequence_bytes.as_slice()
403    };
404    let length_value = u16::try_from(encrypted_record.len())
405        .map_err(|_| RecordError::RecordOverflow)?
406        .to_be_bytes();
407    let length = if header.length_present {
408        length_value.as_slice()
409    } else {
410        &[]
411    };
412    let mut cursor = WriteCursor::new(output);
413    cursor
414        .write_parts(&[
415            &first_bytes,
416            header.connection_id,
417            sequence,
418            length,
419            encrypted_record,
420        ])
421        .map_err(|_| RecordError::InsufficientOutput)?;
422    Ok(total)
423}
424
425fn require_dtls(policy: WirePolicy) -> Result<(), RecordError> {
426    if matches!(policy.version().family(), ProtocolFamily::Dtls) {
427        Ok(())
428    } else {
429        Err(RecordError::ProfileMismatch)
430    }
431}
432
433fn read_byte(cursor: &mut ReadCursor<'_>) -> Result<u8, RecordError> {
434    cursor
435        .take(1)
436        .map_err(|_| RecordError::Truncated)?
437        .first()
438        .copied()
439        .ok_or(RecordError::Truncated)
440}
441
442fn read_u16(cursor: &mut ReadCursor<'_>) -> Result<u16, RecordError> {
443    let bytes = cursor
444        .take_array::<2>()
445        .map_err(|_| RecordError::Truncated)?;
446    Ok(u16::from_be_bytes(*bytes))
447}
448
449fn read_version(cursor: &mut ReadCursor<'_>) -> Result<LegacyRecordVersion, RecordError> {
450    let bytes = cursor
451        .take_array::<2>()
452        .map_err(|_| RecordError::Truncated)?;
453    Ok(LegacyRecordVersion::from_bytes(*bytes))
454}
455
456fn validate_plaintext_length(content_type: ContentType, length: usize) -> Result<(), RecordError> {
457    if length > MAX_PLAINTEXT_LENGTH {
458        return Err(RecordError::RecordOverflow);
459    }
460    if length == 0 && !matches!(content_type, ContentType::ApplicationData) {
461        return Err(RecordError::EmptyFragment);
462    }
463    Ok(())
464}
465
466fn validate_ciphertext_length(length: usize) -> Result<(), RecordError> {
467    if length > MAX_TLS13_CIPHERTEXT_LENGTH {
468        Err(RecordError::RecordOverflow)
469    } else if length == 0 {
470        Err(RecordError::EmptyFragment)
471    } else {
472        Ok(())
473    }
474}