iris_core/protocols/stream/tls/mod.rs
1//! TLS handshake parsing.
2
3mod handshake;
4pub mod parser;
5
6pub use self::handshake::*;
7
8use itertools::Itertools;
9use serde::Serialize;
10use tls_parser::{TlsCipherSuite, TlsState};
11
12/// Serializes [`TlsState`] using its `Debug` representation, since the upstream `tls-parser`
13/// crate does not implement `Serialize` for it.
14fn serialize_state<S: serde::Serializer>(state: &TlsState, s: S) -> Result<S::Ok, S::Error> {
15 s.serialize_str(&format!("{:?}", state))
16}
17
18/// GREASE values. See [RFC 8701](https://datatracker.ietf.org/doc/html/rfc8701).
19const GREASE_TABLE: &[u16] = &[
20 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba,
21 0xcaca, 0xdada, 0xeaea, 0xfafa,
22];
23
24/// Parsed TLS handshake contents.
25#[derive(Debug, Default, Serialize, Clone)]
26pub struct Tls {
27 /// ClientHello message.
28 pub client_hello: Option<ClientHello>,
29 /// ServerHello message.
30 pub server_hello: Option<ServerHello>,
31
32 /// Server Certificate chain.
33 pub server_certificates: Vec<Certificate>,
34 /// Client Certificate chain.
35 pub client_certificates: Vec<Certificate>,
36
37 /// ServerKeyExchange message (TLS 1.2 or earlier).
38 pub server_key_exchange: Option<ServerKeyExchange>,
39 /// ClientKeyExchange message (TLS 1.2 or earlier).
40 pub client_key_exchange: Option<ClientKeyExchange>,
41
42 /// TLS state.
43 #[serde(serialize_with = "serialize_state")]
44 state: TlsState,
45 /// TCP chunks defragmentation buffer. Defragments TCP segments that arrive over multiple
46 /// packets.
47 #[serde(skip)]
48 tcp_buffer: Vec<u8>,
49 /// TLS record defragmentation buffer. Defragments TLS records that arrive over multiple
50 /// segments.
51 #[serde(skip)]
52 record_buffer: Vec<u8>,
53 /// Offset of the start of ciphertext (end of headers) in last-processed segment.
54 /// This will only be (possibly) relevant for last packet in the TLS handshake.
55 /// This can be inaccurate under 0-RTT est. or unsupported extensions.
56 #[serde(skip)]
57 last_body_offset: Option<usize>,
58}
59
60impl Tls {
61 /// Returns the version identifier specified in the ClientHello, or `0` if no ClientHello was
62 /// observed in the handshake.
63 ///
64 /// ## Remarks
65 /// This method returns the message protocol version identifier sent in the ClientHello message,
66 /// not the record protocol version. This value may also differ from the negotiated handshake
67 /// version, such as in the case of TLS 1.3.
68 pub fn client_version(&self) -> u16 {
69 match &self.client_hello {
70 Some(client_hello) => client_hello.version.0,
71 None => 0,
72 }
73 }
74
75 /// Returns the hex-encoded client random, or `""` if no ClientHello was observed in the
76 /// handshake.
77 pub fn client_random(&self) -> String {
78 match &self.client_hello {
79 Some(client_hello) => hex::encode(&client_hello.random),
80 None => "".to_string(),
81 }
82 }
83
84 /// Returns the list of cipher suite names supported by the client.
85 ///
86 /// See [Transport Layer Security (TLS)
87 /// Parameters](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml) for a list
88 /// of TLS cipher suites.
89 pub fn client_ciphers(&self) -> Vec<String> {
90 match &self.client_hello {
91 Some(client_hello) => client_hello
92 .cipher_suites
93 .iter()
94 .map(|c| format!("{}", c))
95 .collect(),
96 None => vec![],
97 }
98 }
99
100 /// Returns the list of compression method identifiers supported by the client.
101 pub fn client_compression_algs(&self) -> Vec<u8> {
102 match &self.client_hello {
103 Some(client_hello) => client_hello.compression_algs.iter().map(|c| c.0).collect(),
104 None => vec![],
105 }
106 }
107
108 /// Returns the list of ALPN protocol names supported by the client.
109 pub fn client_alpn_protocols(&self) -> &[String] {
110 match &self.client_hello {
111 Some(client_hello) => client_hello.alpn_protocols.as_slice(),
112 None => &[],
113 }
114 }
115
116 /// Returns the list of signature algorithm names supported by the client.
117 ///
118 /// See [Transport Layer Security (TLS)
119 /// Parameters](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml) for a list
120 /// of TLS signature algorithms.
121 pub fn client_signature_algs(&self) -> Vec<String> {
122 match &self.client_hello {
123 Some(client_hello) => client_hello
124 .signature_algs
125 .iter()
126 .map(|s| format!("{}", s))
127 .collect(),
128 None => vec![],
129 }
130 }
131
132 /// Returns the list of extension names sent by the client.
133 ///
134 /// See [Transport Layer Security (TLS)
135 /// Extensions](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml)
136 /// for a list of TLS extensions.
137 pub fn client_extensions(&self) -> Vec<String> {
138 match &self.client_hello {
139 Some(client_hello) => client_hello
140 .extension_list
141 .iter()
142 .map(|e| e.to_string())
143 .collect(),
144 None => vec![],
145 }
146 }
147
148 /// Returns `true` if the handshake state machine encountered an invalid or out-of-order
149 /// message sequence.
150 ///
151 /// ## Remarks
152 /// A handshake can still be fully parsed (e.g. a valid ClientHello/SNI) even if a later
153 /// message renders the overall state invalid, so this should be checked in addition to
154 /// inspecting the parsed handshake contents.
155 pub fn is_invalid(&self) -> bool {
156 self.state == TlsState::Invalid
157 }
158
159 /// Returns the name of the server the client is trying to connect to.
160 ///
161 /// ## Remarks
162 /// This method returns the first server name in the server name list.
163 pub fn sni(&self) -> &str {
164 match &self.client_hello {
165 Some(client_hello) => match &client_hello.server_name {
166 Some(sni) => sni.as_str(),
167 None => "",
168 },
169 None => "",
170 }
171 }
172
173 /// Returns the version identifier specified in the ServerHello, or `0` if no ServerHello was
174 /// observed in the handshake.
175 ///
176 /// ## Remarks
177 /// This method returns the message protocol version identifier sent in the ServerHello message,
178 /// not the record protocol version. This value may also differ from the negotiated handshake
179 /// version, such as in the case of TLS 1.3.
180 pub fn server_version(&self) -> u16 {
181 match &self.server_hello {
182 Some(server_hello) => server_hello.version.0,
183 None => 0,
184 }
185 }
186
187 /// Returns the hex-encoded server random, or `""` if no ServerHello was observed in the
188 /// handshake.
189 pub fn server_random(&self) -> String {
190 match &self.server_hello {
191 Some(server_hello) => hex::encode(&server_hello.random),
192 None => "".to_string(),
193 }
194 }
195
196 /// Returns the cipher suite name chosen by the server, or `""` if no ServerHello was observed
197 /// in the handshake.
198 pub fn cipher(&self) -> String {
199 match &self.server_hello {
200 Some(server_hello) => format!("{}", server_hello.cipher_suite),
201 None => "".to_string(),
202 }
203 }
204
205 /// Returns the cipher suite chosen by the server, or `None` if no ServerHello was observed in
206 /// the handshake.
207 pub fn cipher_suite(&self) -> Option<&'static TlsCipherSuite> {
208 match &self.server_hello {
209 Some(server_hello) => server_hello.cipher_suite.get_ciphersuite(),
210 None => None,
211 }
212 }
213
214 /// Returns the compression method identifier chosen by the server, or `0` if no ServerHello was
215 /// observed in the handshake.
216 pub fn compression_alg(&self) -> u8 {
217 match &self.server_hello {
218 Some(server_hello) => server_hello.compression_alg.0,
219 None => 0,
220 }
221 }
222
223 /// Returns the list of extension names sent by the server.
224 ///
225 /// See [Transport Layer Security (TLS)
226 /// Extensions](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml)
227 /// for a list of TLS extensions.
228 pub fn server_extensions(&self) -> Vec<String> {
229 match &self.server_hello {
230 Some(server_hello) => server_hello
231 .extension_list
232 .iter()
233 .map(|e| e.to_string())
234 .collect(),
235 None => vec![],
236 }
237 }
238
239 /// Returns the negotiated TLS handshake version identifier, or `0` if none was identified.
240 ///
241 /// ## Remarks
242 /// Iris supports parsing SSL 3.0 up to TLS 1.3. This method returns the negotiated handshake
243 /// version identifier, even if it does not correspond to a major TLS version (e.g., a draft or
244 /// bespoke version number).
245 pub fn version(&self) -> u16 {
246 match (&self.client_hello, &self.server_hello) {
247 (_ch, Some(sh)) => match sh.selected_version {
248 Some(version) => version.0,
249 None => sh.version.0,
250 },
251 (Some(ch), None) => ch.version.0,
252 (None, None) => 0,
253 }
254 }
255
256 /// Returns the client JA3 string, or `""` if no ClientHello was observed.
257 ///
258 /// ## Remarks
259 /// The JA3 string is defined as the concatenation of:
260 /// `TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats`. See
261 /// [salesforce/ja3](https://github.com/salesforce/ja3) for more details.
262 pub fn ja3_str(&self) -> String {
263 match &self.client_hello {
264 Some(ch) => {
265 format!(
266 "{},{},{},{},{}",
267 ch.version.0,
268 ch.cipher_suites
269 .iter()
270 .map(|x| x.0)
271 .filter(|x| !GREASE_TABLE.contains(x))
272 .join("-"),
273 ch.extension_list
274 .iter()
275 .map(|x| x.0)
276 .filter(|x| !GREASE_TABLE.contains(x))
277 .join("-"),
278 ch.supported_groups
279 .iter()
280 .map(|x| x.0)
281 .filter(|x| !GREASE_TABLE.contains(x))
282 .join("-"),
283 ch.ec_point_formats.iter().join("-"),
284 )
285 }
286 None => "".to_string(),
287 }
288 }
289
290 /// Returns the server JA3S string, or `""` if no ServerHello was observed.
291 ///
292 /// ## Remarks
293 /// The JA3S string is defined as the concatenation of: `TLSVersion,Cipher,Extensions`. See
294 /// [salesforce/ja3](https://github.com/salesforce/ja3) for more details.
295 pub fn ja3s_str(&self) -> String {
296 match &self.server_hello {
297 Some(sh) => {
298 format!(
299 "{},{},{}",
300 sh.version.0,
301 sh.cipher_suite.0,
302 sh.extension_list
303 .iter()
304 .map(|x| x.0)
305 .filter(|x| !GREASE_TABLE.contains(x))
306 .join("-")
307 )
308 }
309 None => "".to_string(),
310 }
311 }
312
313 /// Returns the JA3 fingerprint.
314 pub fn ja3_hash(&self) -> String {
315 format!("{:x}", md5::compute(self.ja3_str()))
316 }
317
318 /// Returns the JA3S fingerprint.
319 pub fn ja3s_hash(&self) -> String {
320 format!("{:x}", md5::compute(self.ja3s_str()))
321 }
322}