⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/net/tls/
record.rs

1//! TLS 1.3 Record Layer (RFC 8446 Section 5)
2//!
3//! Handles record framing, fragment reassembly, and encrypted record
4//! wrapping/unwrapping for TLS 1.3.
5
6#[cfg(feature = "alloc")]
7use alloc::vec::Vec;
8
9use super::{
10    cipher::{aead_decrypt, aead_encrypt},
11    CipherSuite, AEAD_TAG_LEN, MAX_RECORD_SIZE, NONCE_LEN, TLS_LEGACY_VERSION,
12};
13
14// ============================================================================
15// Content Types
16// ============================================================================
17
18/// TLS record content types (RFC 8446 Section 5.1)
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(u8)]
21pub enum ContentType {
22    ChangeCipherSpec = 20,
23    Alert = 21,
24    Handshake = 22,
25    ApplicationData = 23,
26}
27
28impl ContentType {
29    pub(crate) fn from_u8(v: u8) -> Option<Self> {
30        match v {
31            20 => Some(Self::ChangeCipherSpec),
32            21 => Some(Self::Alert),
33            22 => Some(Self::Handshake),
34            23 => Some(Self::ApplicationData),
35            _ => None,
36        }
37    }
38}
39
40// ============================================================================
41// Record Header
42// ============================================================================
43
44/// TLS record header (5 bytes on the wire)
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct RecordHeader {
47    pub content_type: ContentType,
48    pub legacy_version: u16,
49    pub length: u16,
50}
51
52impl RecordHeader {
53    /// Encode record header to bytes
54    pub fn encode(&self, buf: &mut [u8]) -> usize {
55        if buf.len() < 5 {
56            return 0;
57        }
58        buf[0] = self.content_type as u8;
59        buf[1..3].copy_from_slice(&self.legacy_version.to_be_bytes());
60        buf[3..5].copy_from_slice(&self.length.to_be_bytes());
61        5
62    }
63
64    /// Decode record header from bytes
65    pub fn decode(buf: &[u8]) -> Option<Self> {
66        if buf.len() < 5 {
67            return None;
68        }
69        let content_type = ContentType::from_u8(buf[0])?;
70        let legacy_version = u16::from_be_bytes([buf[1], buf[2]]);
71        let length = u16::from_be_bytes([buf[3], buf[4]]);
72
73        // Enforce max record size (payload + optional tag)
74        if length as usize > MAX_RECORD_SIZE + AEAD_TAG_LEN + 1 {
75            return None;
76        }
77
78        Some(Self {
79            content_type,
80            legacy_version,
81            length,
82        })
83    }
84}
85
86// ============================================================================
87// TLS Record
88// ============================================================================
89
90/// TLS plaintext record
91#[derive(Debug, Clone)]
92pub struct TlsRecord {
93    pub content_type: ContentType,
94    pub fragment: Vec<u8>,
95}
96
97impl TlsRecord {
98    /// Create a new record with the given content type and payload
99    pub fn new(content_type: ContentType, fragment: Vec<u8>) -> Self {
100        Self {
101            content_type,
102            fragment,
103        }
104    }
105
106    /// Encode the record to wire format (header + fragment)
107    pub fn encode(&self) -> Vec<u8> {
108        let header = RecordHeader {
109            content_type: self.content_type,
110            legacy_version: TLS_LEGACY_VERSION,
111            length: self.fragment.len() as u16,
112        };
113        let mut buf = alloc::vec![0u8; 5 + self.fragment.len()];
114        header.encode(&mut buf);
115        buf[5..].copy_from_slice(&self.fragment);
116        buf
117    }
118
119    /// Decode a record from wire format
120    pub fn decode(data: &[u8]) -> Option<(Self, usize)> {
121        let header = RecordHeader::decode(data)?;
122        let total_len = 5 + header.length as usize;
123        if data.len() < total_len {
124            return None;
125        }
126        let fragment = data[5..total_len].to_vec();
127        Some((
128            Self {
129                content_type: header.content_type,
130                fragment,
131            },
132            total_len,
133        ))
134    }
135}
136
137// ============================================================================
138// Fragment Reassembly
139// ============================================================================
140
141/// Fragment reassembly buffer for handshake messages that span multiple records
142pub struct FragmentBuffer {
143    buffer: Vec<u8>,
144    expected_type: Option<ContentType>,
145}
146
147impl Default for FragmentBuffer {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl FragmentBuffer {
154    pub fn new() -> Self {
155        Self {
156            buffer: Vec::new(),
157            expected_type: None,
158        }
159    }
160
161    /// Append a record fragment. Returns the reassembled message when complete.
162    pub fn append(&mut self, record: &TlsRecord) -> Option<Vec<u8>> {
163        match self.expected_type {
164            None => {
165                self.expected_type = Some(record.content_type);
166            }
167            Some(ct) if ct != record.content_type => {
168                // Content type mismatch -- reset
169                self.buffer.clear();
170                self.expected_type = Some(record.content_type);
171            }
172            _ => {}
173        }
174
175        self.buffer.extend_from_slice(&record.fragment);
176
177        // For handshake messages, check if we have a complete message
178        if record.content_type == ContentType::Handshake && self.buffer.len() >= 4 {
179            let msg_len = ((self.buffer[1] as usize) << 16)
180                | ((self.buffer[2] as usize) << 8)
181                | (self.buffer[3] as usize);
182            let total = 4 + msg_len;
183            if self.buffer.len() >= total {
184                let message = self.buffer[..total].to_vec();
185                self.buffer = self.buffer[total..].to_vec();
186                if self.buffer.is_empty() {
187                    self.expected_type = None;
188                }
189                return Some(message);
190            }
191        }
192
193        None
194    }
195
196    /// Reset the fragment buffer
197    pub fn reset(&mut self) {
198        self.buffer.clear();
199        self.expected_type = None;
200    }
201}
202
203// ============================================================================
204// Encrypted Record Operations
205// ============================================================================
206
207/// Wrap a plaintext record into an encrypted TLS 1.3 record.
208///
209/// TLS 1.3 encrypted records have content_type = ApplicationData on the wire,
210/// with the real content_type appended after the plaintext before encryption.
211pub fn encrypt_record(
212    record: &TlsRecord,
213    key: &[u8],
214    iv: &[u8; NONCE_LEN],
215    seq_num: u64,
216    cipher: CipherSuite,
217) -> Option<TlsRecord> {
218    // Build inner plaintext: fragment || content_type
219    let mut inner = record.fragment.clone();
220    inner.push(record.content_type as u8);
221
222    // Compute per-record nonce: IV XOR sequence number (RFC 8446 Section 5.3)
223    let nonce = compute_nonce(iv, seq_num);
224
225    // Additional data is the record header of the outer (encrypted) record
226    let encrypted_len = inner.len() + AEAD_TAG_LEN;
227    let mut aad = [0u8; 5];
228    aad[0] = ContentType::ApplicationData as u8;
229    aad[1..3].copy_from_slice(&TLS_LEGACY_VERSION.to_be_bytes());
230    aad[3..5].copy_from_slice(&(encrypted_len as u16).to_be_bytes());
231
232    let ciphertext = aead_encrypt(cipher, key, &nonce, &aad, &inner)?;
233
234    Some(TlsRecord::new(ContentType::ApplicationData, ciphertext))
235}
236
237/// Unwrap an encrypted TLS 1.3 record back to plaintext.
238pub fn decrypt_record(
239    record: &TlsRecord,
240    key: &[u8],
241    iv: &[u8; NONCE_LEN],
242    seq_num: u64,
243    cipher: CipherSuite,
244) -> Option<TlsRecord> {
245    if record.content_type != ContentType::ApplicationData {
246        return None;
247    }
248
249    let nonce = compute_nonce(iv, seq_num);
250
251    // Reconstruct AAD from wire header
252    let mut aad = [0u8; 5];
253    aad[0] = ContentType::ApplicationData as u8;
254    aad[1..3].copy_from_slice(&TLS_LEGACY_VERSION.to_be_bytes());
255    aad[3..5].copy_from_slice(&(record.fragment.len() as u16).to_be_bytes());
256
257    let plaintext = aead_decrypt(cipher, key, &nonce, &aad, &record.fragment)?;
258
259    if plaintext.is_empty() {
260        return None;
261    }
262
263    // Last byte of plaintext is the real content type
264    let real_ct = ContentType::from_u8(*plaintext.last()?)?;
265    let payload = plaintext[..plaintext.len() - 1].to_vec();
266
267    Some(TlsRecord::new(real_ct, payload))
268}
269
270/// Compute per-record nonce: IV XOR (zero-padded 64-bit sequence number)
271pub(crate) fn compute_nonce(iv: &[u8; NONCE_LEN], seq_num: u64) -> [u8; NONCE_LEN] {
272    let mut nonce = *iv;
273    let seq_bytes = seq_num.to_be_bytes();
274    // XOR sequence number into the last 8 bytes of the IV
275    for i in 0..8 {
276        nonce[NONCE_LEN - 8 + i] ^= seq_bytes[i];
277    }
278    nonce
279}