Skip to main content

brynja_protocol/tls/
record.rs

1//! TLS stream record framing.
2
3use brynja_core::{ProtocolFamily, ProtocolVersion, ReadCursor, WriteCursor};
4
5use super::{
6    ContentType, ContentTypeCode, MAX_PLAINTEXT_LENGTH, MAX_TLS12_CIPHERTEXT_LENGTH,
7    MAX_TLS13_CIPHERTEXT_LENGTH, RecordError, WirePolicy,
8};
9
10const HEADER_LENGTH: usize = 5;
11
12/// Two preserved legacy record-version bytes.
13///
14/// Parsing TLS 1.3 plaintext preserves this field but deliberately does not
15/// use it for version selection or validation, as required by RFC 9846.
16#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
17pub struct LegacyRecordVersion([u8; 2]);
18
19/// One borrowed unprotected TLS record.
20///
21/// The fragment aliases the caller's input and is not copied or allocated.
22///
23/// ```compile_fail
24/// let bytes = [22, 3, 3, 0, 1, 0];
25/// let policy = brynja_protocol::WirePolicy::for_version(
26///     brynja_core::ProtocolVersion::Tls13,
27/// );
28/// let (record, _) = brynja_protocol::TlsPlaintext::parse(policy, &bytes).unwrap();
29/// println!("{record:?}");
30/// ```
31#[derive(Clone, Copy, Eq, PartialEq)]
32pub struct TlsPlaintext<'input> {
33    content_type: ContentType,
34    legacy_record_version: LegacyRecordVersion,
35    fragment: &'input [u8],
36}
37
38/// One borrowed protected TLS record.
39#[derive(Clone, Copy, Eq, PartialEq)]
40pub struct TlsCiphertext<'input> {
41    content_type: ContentType,
42    legacy_record_version: LegacyRecordVersion,
43    fragment: &'input [u8],
44}
45
46impl LegacyRecordVersion {
47    /// Preserves two caller-provided bytes without interpreting a version.
48    #[must_use]
49    pub const fn from_bytes(bytes: [u8; 2]) -> Self {
50        Self(bytes)
51    }
52
53    /// Returns the exact preserved wire bytes.
54    #[must_use]
55    pub const fn bytes(self) -> [u8; 2] {
56        self.0
57    }
58
59    /// Returns the default legacy version emitted by TLS 1.3 records.
60    #[must_use]
61    pub const fn tls13_default() -> Self {
62        Self([3, 3])
63    }
64
65    /// Returns the compatibility value permitted for an initial ClientHello.
66    #[must_use]
67    pub const fn tls13_initial_client_hello() -> Self {
68        Self([3, 1])
69    }
70
71    /// Returns the default legacy version emitted by DTLS 1.3 plaintext.
72    #[must_use]
73    pub const fn dtls13_default() -> Self {
74        Self([254, 253])
75    }
76
77    /// Returns the compatibility value permitted for an initial ClientHello.
78    #[must_use]
79    pub const fn dtls13_initial_client_hello() -> Self {
80        Self([254, 255])
81    }
82}
83
84impl<'input> TlsPlaintext<'input> {
85    /// Parses one TLS plaintext record under an already selected policy.
86    ///
87    /// Success returns the exact unconsumed stream suffix. Failure has no
88    /// caller-visible cursor mutation.
89    pub fn parse(
90        policy: WirePolicy,
91        input: &'input [u8],
92    ) -> Result<(Self, &'input [u8]), RecordError> {
93        require_tls(policy)?;
94        let mut cursor = ReadCursor::new(input);
95        let content_type = read_content_type(&mut cursor, policy, false)?;
96        let version = read_version(&mut cursor)?;
97        let length = read_u16(&mut cursor)?;
98        validate_plaintext_length(content_type, length)?;
99        let fragment = take(&mut cursor, length)?;
100        let remaining = cursor.remaining();
101        Ok((
102            Self {
103                content_type,
104                legacy_record_version: version,
105                fragment,
106            },
107            remaining,
108        ))
109    }
110
111    /// Constructs a checked plaintext envelope for encoding.
112    pub fn new(
113        policy: WirePolicy,
114        content_type: ContentTypeCode,
115        legacy_record_version: LegacyRecordVersion,
116        fragment: &'input [u8],
117    ) -> Result<Self, RecordError> {
118        require_tls(policy)?;
119        let content_type = policy.admit_plaintext(content_type)?;
120        validate_plaintext_length(content_type, fragment.len())?;
121        if matches!(policy.version(), ProtocolVersion::Tls13)
122            && !matches!(legacy_record_version.bytes(), [3, 3] | [3, 1])
123        {
124            return Err(RecordError::InvalidPlaintextVersion);
125        }
126        Ok(Self {
127            content_type,
128            legacy_record_version,
129            fragment,
130        })
131    }
132
133    /// Returns the admitted content type.
134    #[must_use]
135    pub const fn content_type(&self) -> ContentType {
136        self.content_type
137    }
138
139    /// Returns the preserved, non-negotiating legacy bytes.
140    #[must_use]
141    pub const fn legacy_record_version(&self) -> LegacyRecordVersion {
142        self.legacy_record_version
143    }
144
145    /// Returns the exact borrowed fragment.
146    #[must_use]
147    pub const fn fragment(&self) -> &'input [u8] {
148        self.fragment
149    }
150
151    /// Returns the complete encoded length.
152    #[must_use]
153    pub const fn encoded_len(&self) -> usize {
154        HEADER_LENGTH.saturating_add(self.fragment.len())
155    }
156
157    /// Writes the complete record transactionally into caller storage.
158    pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
159        encode_record(
160            self.content_type,
161            self.legacy_record_version,
162            self.fragment,
163            output,
164        )
165    }
166}
167
168impl<'input> TlsCiphertext<'input> {
169    /// Parses one protected TLS 1.2 or TLS 1.3 record.
170    pub fn parse(
171        policy: WirePolicy,
172        input: &'input [u8],
173    ) -> Result<(Self, &'input [u8]), RecordError> {
174        require_tls(policy)?;
175        let mut cursor = ReadCursor::new(input);
176        let content_type = read_content_type(&mut cursor, policy, true)?;
177        let version = read_version(&mut cursor)?;
178        validate_ciphertext_version(policy, version)?;
179        let length = read_u16(&mut cursor)?;
180        validate_ciphertext_length(policy, length)?;
181        let fragment = take(&mut cursor, length)?;
182        let remaining = cursor.remaining();
183        Ok((
184            Self {
185                content_type,
186                legacy_record_version: version,
187                fragment,
188            },
189            remaining,
190        ))
191    }
192
193    /// Constructs a checked protected envelope for encoding.
194    pub fn new(
195        policy: WirePolicy,
196        content_type: ContentTypeCode,
197        legacy_record_version: LegacyRecordVersion,
198        fragment: &'input [u8],
199    ) -> Result<Self, RecordError> {
200        require_tls(policy)?;
201        let content_type = policy.admit_ciphertext(content_type)?;
202        validate_ciphertext_version(policy, legacy_record_version)?;
203        validate_ciphertext_length(policy, fragment.len())?;
204        Ok(Self {
205            content_type,
206            legacy_record_version,
207            fragment,
208        })
209    }
210
211    /// Returns the admitted outer content type.
212    #[must_use]
213    pub const fn content_type(&self) -> ContentType {
214        self.content_type
215    }
216
217    /// Returns the exact outer legacy-version bytes.
218    #[must_use]
219    pub const fn legacy_record_version(&self) -> LegacyRecordVersion {
220        self.legacy_record_version
221    }
222
223    /// Returns the exact borrowed protected fragment.
224    #[must_use]
225    pub const fn fragment(&self) -> &'input [u8] {
226        self.fragment
227    }
228
229    /// Returns the complete encoded length.
230    #[must_use]
231    pub const fn encoded_len(&self) -> usize {
232        HEADER_LENGTH.saturating_add(self.fragment.len())
233    }
234
235    /// Writes the complete record transactionally into caller storage.
236    pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
237        encode_record(
238            self.content_type,
239            self.legacy_record_version,
240            self.fragment,
241            output,
242        )
243    }
244}
245
246fn require_tls(policy: WirePolicy) -> Result<(), RecordError> {
247    if matches!(policy.version().family(), ProtocolFamily::Tls) {
248        Ok(())
249    } else {
250        Err(RecordError::ProfileMismatch)
251    }
252}
253
254fn read_content_type(
255    cursor: &mut ReadCursor<'_>,
256    policy: WirePolicy,
257    ciphertext: bool,
258) -> Result<ContentType, RecordError> {
259    let bytes = take(cursor, 1)?;
260    let code = match bytes.first() {
261        Some(code) => ContentTypeCode::classify(*code),
262        None => return Err(RecordError::Truncated),
263    };
264    if ciphertext {
265        policy.admit_ciphertext(code)
266    } else {
267        policy.admit_plaintext(code)
268    }
269}
270
271fn read_version(cursor: &mut ReadCursor<'_>) -> Result<LegacyRecordVersion, RecordError> {
272    let bytes = cursor
273        .take_array::<2>()
274        .map_err(|_| RecordError::Truncated)?;
275    Ok(LegacyRecordVersion::from_bytes(*bytes))
276}
277
278fn read_u16(cursor: &mut ReadCursor<'_>) -> Result<usize, RecordError> {
279    let bytes = cursor
280        .take_array::<2>()
281        .map_err(|_| RecordError::Truncated)?;
282    Ok(usize::from(u16::from_be_bytes(*bytes)))
283}
284
285fn take<'input>(
286    cursor: &mut ReadCursor<'input>,
287    length: usize,
288) -> Result<&'input [u8], RecordError> {
289    cursor.take(length).map_err(|_| RecordError::Truncated)
290}
291
292fn validate_plaintext_length(content_type: ContentType, length: usize) -> Result<(), RecordError> {
293    if length > MAX_PLAINTEXT_LENGTH {
294        return Err(RecordError::RecordOverflow);
295    }
296    if length == 0 && !matches!(content_type, ContentType::ApplicationData) {
297        return Err(RecordError::EmptyFragment);
298    }
299    Ok(())
300}
301
302fn validate_ciphertext_version(
303    policy: WirePolicy,
304    version: LegacyRecordVersion,
305) -> Result<(), RecordError> {
306    if matches!(policy.version(), ProtocolVersion::Tls13) && version.bytes() != [3, 3] {
307        Err(RecordError::InvalidCiphertextVersion)
308    } else {
309        Ok(())
310    }
311}
312
313fn validate_ciphertext_length(policy: WirePolicy, length: usize) -> Result<(), RecordError> {
314    let maximum = if matches!(policy.version(), ProtocolVersion::Tls13) {
315        MAX_TLS13_CIPHERTEXT_LENGTH
316    } else {
317        MAX_TLS12_CIPHERTEXT_LENGTH
318    };
319    if length > maximum {
320        Err(RecordError::RecordOverflow)
321    } else if length == 0 && matches!(policy.version(), ProtocolVersion::Tls13) {
322        Err(RecordError::EmptyFragment)
323    } else {
324        Ok(())
325    }
326}
327
328fn encode_record(
329    content_type: ContentType,
330    version: LegacyRecordVersion,
331    fragment: &[u8],
332    output: &mut [u8],
333) -> Result<usize, RecordError> {
334    let total = HEADER_LENGTH
335        .checked_add(fragment.len())
336        .ok_or(RecordError::LengthOverflow)?;
337    if output.len() < total {
338        return Err(RecordError::InsufficientOutput);
339    }
340    let length = u16::try_from(fragment.len()).map_err(|_| RecordError::RecordOverflow)?;
341    let type_bytes = [content_type.code()];
342    let version_bytes = version.bytes();
343    let length_bytes = length.to_be_bytes();
344    let mut cursor = WriteCursor::new(output);
345    cursor
346        .write_parts(&[&type_bytes, &version_bytes, &length_bytes, fragment])
347        .map_err(|_| RecordError::InsufficientOutput)?;
348    Ok(total)
349}