Skip to main content

wireforge_app/tls/
client_hello.rs

1//! TLS ClientHello parser with SNI and extensions support.
2
3use super::extensions::TlsExtensionsIter;
4use super::handshake::TlsHandshake;
5use super::record::TlsRecord;
6
7#[derive(Debug, Clone)]
8pub struct TlsClientHello<'a> { buf: &'a [u8], client_hello_start: usize }
9
10impl<'a> TlsClientHello<'a> {
11    /// Parse a TLS ClientHello from raw bytes (Record → Handshake → ClientHello).
12    pub fn parse(buf: &'a [u8]) -> Option<Self> {
13        let record = TlsRecord::parse(buf)?;
14        let handshake = TlsHandshake::parse(record.fragment())?;
15        if handshake.handshake_type() != 1 { return None; } // not client_hello
16        let body = handshake.body();
17        if body.len() < 38 { return None; } // minimum: version(2)+random(32)+session_id_len(1)+cipher_suites_len(2)+comp_len(1)
18        Some(Self { buf, client_hello_start: body.as_ptr() as usize - buf.as_ptr() as usize })
19    }
20
21    fn body(&self) -> &'a [u8] { &self.buf[self.client_hello_start..] }
22
23    pub fn client_version(&self) -> u16 {
24        let b = self.body();
25        u16::from_be_bytes([b[0], b[1]])
26    }
27
28    pub fn random(&self) -> Option<&[u8; 32]> {
29        self.body().get(2..34)?.try_into().ok()
30    }
31
32    pub fn session_id(&self) -> &[u8] {
33        let b = self.body();
34        let len = b[34] as usize;
35        let end = (35 + len).min(b.len());
36        &b[35..end]
37    }
38
39    pub fn cipher_suites(&self) -> &[u16] {
40        let b = self.body();
41        let pos = 35 + b[34] as usize; // skip session_id
42        if pos + 2 > b.len() { return &[]; }
43        let len = u16::from_be_bytes([b[pos], b[pos+1]]) as usize;
44        let data = &b[pos+2..];
45        let count = (len / 2).min(data.len() / 2);
46        // SAFETY: transmuting &[u8] to &[u16] — data is aligned for u16 reads
47        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u16, count) }
48    }
49
50    pub fn compression_methods(&self) -> &[u8] {
51        let b = self.body();
52        let mut pos = 35 + b[34] as usize;
53        if pos + 2 > b.len() { return &[]; }
54        let cs_len = u16::from_be_bytes([b[pos], b[pos+1]]) as usize;
55        pos += 2 + cs_len;
56        if pos + 1 > b.len() { return &[]; }
57        let cm_len = b[pos] as usize;
58        let end = (pos + 1 + cm_len).min(b.len());
59        &b[pos+1..end]
60    }
61
62    /// Iterate over TLS extensions.
63    pub fn extensions(&self) -> TlsExtensionsIter<'a> {
64        let b = self.body();
65        let mut pos = 35 + b[34] as usize;
66        if pos + 2 > b.len() { return TlsExtensionsIter::new(&[]); }
67        let cs_len = u16::from_be_bytes([b[pos], b[pos+1]]) as usize;
68        pos += 2 + cs_len;
69        if pos + 1 > b.len() { return TlsExtensionsIter::new(&[]); }
70        let cm_len = b[pos] as usize;
71        pos += 1 + cm_len;
72        if pos + 2 > b.len() { return TlsExtensionsIter::new(&[]); }
73        let ext_len = u16::from_be_bytes([b[pos], b[pos+1]]) as usize;
74        let end = (pos + 2 + ext_len).min(b.len());
75        TlsExtensionsIter::new(&b[pos+2..end])
76    }
77
78    /// Extract SNI server name from extension type 0x0000.
79    pub fn server_name(&self) -> Option<&str> {
80        for ext in self.extensions() {
81            if ext.extension_type == 0x0000 && ext.data.len() >= 5 {
82                // server_name_list_length(2) + name_type(1) + name_length(2) + name
83                let list_len = u16::from_be_bytes([ext.data[0], ext.data[1]]) as usize;
84                if list_len + 2 <= ext.data.len() {
85                    let entry = &ext.data[2..2 + list_len];
86                    if entry.len() >= 3 && entry[0] == 0 { // name_type=hostname
87                        let name_len = u16::from_be_bytes([entry[1], entry[2]]) as usize;
88                        if 3 + name_len <= entry.len() {
89                            return std::str::from_utf8(&entry[3..3 + name_len]).ok();
90                        }
91                    }
92                }
93            }
94        }
95        None
96    }
97
98    /// Extract supported_versions from TLS 1.3 extension type 0x002b.
99    pub fn supported_versions(&self) -> Option<Vec<u16>> {
100        for ext in self.extensions() {
101            if ext.extension_type == 0x002b && !ext.data.is_empty() {
102                let len = ext.data[0] as usize;
103                if len < ext.data.len() {
104                    let versions: Vec<u16> = ext.data[1..1+len]
105                        .chunks_exact(2)
106                        .map(|c| u16::from_be_bytes([c[0], c[1]]))
107                        .collect();
108                    return Some(versions);
109                }
110            }
111        }
112        None
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    // TLS 1.2 ClientHello: SNI=example.com, 2 cipher suites, no compression, no extensions beyond SNI
121    fn tls12_sample() -> Vec<u8> {
122        let ch = [
123            0x03u8, 0x03, // version TLS 1.2
124            // random (32 bytes of 0xAA)
125        ];
126        let mut data = vec![0x16, 0x03, 0x03, 0x00, 0x00]; // record with placeholder length
127        let mut ch_body = Vec::new();
128        ch_body.extend_from_slice(&ch);
129        ch_body.extend_from_slice(&[0xAAu8; 32]); // random
130        ch_body.push(0); // session_id_len = 0
131        ch_body.extend_from_slice(&[0x00u8, 0x04]); // cipher_suites_len = 4 (2 suites)
132        ch_body.extend_from_slice(&0xC02Bu16.to_be_bytes()); // TLS_ECDHE_RSA_AES_128_GCM
133        ch_body.extend_from_slice(&0x009Cu16.to_be_bytes()); // TLS_RSA_AES_128_GCM
134        ch_body.push(1); ch_body.push(0); // compression_methods: null
135        // SNI extension
136        let sn = b"example.com";
137        // SNI extension: type=0x0000, len, server_name_list_len, name_type=0, name_len, name
138        let sn_len = sn.len();
139        let sn_ext_len = (sn_len + 5) as u16;
140        let sn_list_len = (sn_len + 3) as u16;
141        let mut ext_data = vec![0x00u8, 0x00]; // type=server_name
142        ext_data.extend_from_slice(&sn_ext_len.to_be_bytes());
143        ext_data.extend_from_slice(&sn_list_len.to_be_bytes());
144        ext_data.push(0x00); // name_type=hostname
145        ext_data.extend_from_slice(&(sn_len as u16).to_be_bytes());
146        ext_data.extend_from_slice(sn);
147        // extensions length prefix
148        ch_body.extend_from_slice(&(ext_data.len() as u16).to_be_bytes());
149        ch_body.extend_from_slice(&ext_data);
150        // Handshake
151        let handshake_len = ch_body.len();
152        data.extend_from_slice(&[0x01]); // handshake_type=client_hello
153        data.extend_from_slice(&(handshake_len as u32).to_be_bytes()[1..]); // 3-byte length
154        data.extend_from_slice(&ch_body);
155        // Record length
156        let rec_len = data.len() - 5;
157        data[3] = (rec_len >> 8) as u8;
158        data[4] = rec_len as u8;
159        data
160    }
161
162    #[test]
163    fn parse_tls12_client_hello() {
164        let data = tls12_sample();
165        let ch = TlsClientHello::parse(&data).unwrap();
166        assert_eq!(ch.client_version(), 0x0303);
167        assert_eq!(ch.cipher_suites().len(), 2);
168        assert_eq!(ch.session_id().len(), 0);
169        assert_eq!(ch.server_name(), Some("example.com"));
170        assert!(ch.supported_versions().is_none());
171    }
172
173    #[test]
174    fn parse_tls13_client_hello() {
175        // Hardcoded valid TLS 1.3 ClientHello with supported_versions extension
176        let data = &[
177            0x16, 0x03, 0x01, 0x00, 0x44, // TLS record: handshake, TLS 1.0 compat, len=68
178            0x01, 0x00, 0x00, 0x40, // handshake: client_hello, len=64
179            0x03, 0x03, // client_version = TLS 1.2 (legacy)
180            // random (32 bytes)
181            0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,
182            0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,
183            0x00, // session_id_len=0
184            0x00, 0x02, 0x13, 0x01, // cipher_suites: 1 suite (TLS_AES_128_GCM)
185            0x01, 0x00, // compression: null
186            0x00, 0x07, // extensions length = 7
187            0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, // supported_versions: TLS 1.3
188        ];
189        let ch = TlsClientHello::parse(data).unwrap();
190        assert!(ch.server_name().is_none());
191        let sv = ch.supported_versions().unwrap();
192        assert_eq!(sv, vec![0x0304]);
193    }
194
195    #[test]
196    fn parse_too_short() {
197        assert!(TlsClientHello::parse(&[]).is_none());
198        assert!(TlsClientHello::parse(&[0x16, 0x03, 0x03, 0x00, 0x00]).is_none());
199    }
200}