1use rama_core::context::Extensions;
2use std::{fmt, io};
3
4use crate::tls::{
5 CipherSuite, ECPointFormat, ExtensionId, ProtocolVersion, SecureTransport, SupportedGroup,
6 client::NegotiatedTlsParameters,
7};
8
9use super::ClientHelloProvider;
10
11#[derive(Debug, Clone)]
12pub struct Ja3 {
17 version: ProtocolVersion,
18 cipher_suites: Vec<CipherSuite>,
19 extensions: Option<Vec<ExtensionId>>,
20 supported_groups: Option<Vec<SupportedGroup>>,
21 ec_point_formats: Option<Vec<ECPointFormat>>,
22}
23
24impl Ja3 {
25 pub fn compute(ext: &Extensions) -> Result<Self, Ja3ComputeError> {
29 let client_hello = ext
30 .get::<SecureTransport>()
31 .and_then(|st| st.client_hello())
32 .ok_or(Ja3ComputeError::MissingClientHello)?;
33 let negotiated_tls_version = ext
34 .get::<NegotiatedTlsParameters>()
35 .map(|param| param.protocol_version);
36 Self::compute_from_client_hello(client_hello, negotiated_tls_version)
37 }
38
39 pub fn compute_from_client_hello(
47 client_hello: impl ClientHelloProvider,
48 negotiated_tls_version: Option<ProtocolVersion>,
49 ) -> Result<Self, Ja3ComputeError> {
50 let version = negotiated_tls_version.unwrap_or_else(|| {
51 tracing::trace!(
52 "negotiated tls protocol version missing: fallback to client hello tls"
53 );
54 client_hello.protocol_version()
55 });
56
57 let cipher_suites: Vec<_> = client_hello
58 .cipher_suites()
59 .filter(|c| !c.is_grease())
60 .collect();
61 if cipher_suites.is_empty() {
62 return Err(Ja3ComputeError::EmptyCipherSuites);
63 }
64
65 let mut extensions = None;
66 let mut supported_groups = None;
67 let mut ec_point_formats = None;
68
69 let ce_extensions = client_hello.extensions();
70 for ext in ce_extensions {
71 if ext.id().is_grease() {
72 continue;
73 }
74
75 extensions.get_or_insert_with(Vec::default).push(ext.id());
76
77 match ext {
78 crate::tls::client::ClientHelloExtension::SupportedGroups(vec) => {
79 let vec: Vec<_> = vec.iter().filter(|g| !g.is_grease()).copied().collect();
80 if !vec.is_empty() {
81 supported_groups = Some(vec)
82 }
83 }
84 crate::tls::client::ClientHelloExtension::ECPointFormats(vec)
85 if !vec.is_empty() =>
86 {
87 ec_point_formats = Some(vec.clone())
88 }
89 _ => (),
90 }
91 }
92
93 Ok(Self {
94 version,
95 cipher_suites,
96 extensions,
97 supported_groups,
98 ec_point_formats,
99 })
100 }
101
102 #[inline]
103 pub fn hash(&self) -> String {
105 format!("{self:x}")
106 }
107
108 fn hash_to(&self, w: &mut impl fmt::Write, lower: bool) -> fmt::Result {
110 let mut ctx = md5::Context::new();
111 let _ = self.write_to_io(&mut ctx).inspect_err(|err| {
112 if cfg!(debug_assertions) {
113 panic!("md5 ingest failed: {err:?}");
114 }
115 });
116 let digest = ctx.compute();
117 if lower {
118 write!(w, "{digest:x}",)?;
119 } else {
120 write!(w, "{digest:X}",)?;
121 }
122 Ok(())
123 }
124}
125
126macro_rules! impl_write_to {
127 ($w:ident, $this:ident) => {{
128 write!($w, "{}", u16::from($this.version))?;
129
130 let mut sep = ',';
131 for cipher_suite in &$this.cipher_suites {
132 write!($w, "{sep}{}", u16::from(*cipher_suite))?;
133 sep = '-';
134 }
135
136 match &$this.extensions {
137 Some(ext) => {
138 sep = ',';
139 for ext in ext {
140 write!($w, "{sep}{}", u16::from(*ext))?;
141 sep = '-';
142 }
143 }
144 None => write!($w, ",")?,
145 }
146
147 match &$this.supported_groups {
148 Some(supported_groups) => {
149 sep = ',';
150 for g in supported_groups {
151 write!($w, "{sep}{}", u16::from(*g))?;
152 sep = '-';
153 }
154 }
155 None => write!($w, ",")?,
156 }
157
158 match &$this.ec_point_formats {
159 Some(ec_point_formats) => {
160 sep = ',';
161 for p in ec_point_formats {
162 write!($w, "{sep}{}", u8::from(*p))?;
163 sep = '-';
164 }
165 }
166 None => write!($w, ",")?,
167 }
168
169 Ok(())
170 }};
171}
172
173impl Ja3 {
174 fn write_to_io(&self, w: &mut impl io::Write) -> io::Result<()> {
175 impl_write_to!(w, self)
176 }
177
178 fn write_to_fmt(&self, w: &mut impl fmt::Write) -> fmt::Result {
179 impl_write_to!(w, self)
180 }
181}
182
183impl fmt::Display for Ja3 {
184 #[inline]
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 self.write_to_fmt(f)
187 }
188}
189
190impl fmt::LowerHex for Ja3 {
191 #[inline]
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 self.hash_to(f, true)?;
194 Ok(())
195 }
196}
197
198impl fmt::UpperHex for Ja3 {
199 #[inline]
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 self.hash_to(f, false)?;
202 Ok(())
203 }
204}
205
206#[derive(Debug, Clone)]
207pub enum Ja3ComputeError {
209 MissingClientHello,
211 EmptyCipherSuites,
213}
214
215impl fmt::Display for Ja3ComputeError {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 match self {
218 Ja3ComputeError::MissingClientHello => {
219 write!(f, "Ja3 Compute Error: missing client hello")
220 }
221 Ja3ComputeError::EmptyCipherSuites => {
222 write!(f, "Ja3 Compute Error: empty cipher suites")
223 }
224 }
225 }
226}
227
228impl std::error::Error for Ja3ComputeError {}
229
230#[cfg(test)]
231mod tests {
232 use crate::tls::client::parse_client_hello;
233
234 use super::*;
235
236 #[derive(Debug)]
237 struct TestCase {
238 client_hello: Vec<u8>,
239 pcap: &'static str,
240 expected_ja3_str: &'static str,
241 expected_ja3_hash: &'static str,
242 }
243
244 #[test]
245 fn test_ja3_compute() {
246 let test_cases = [
248 TestCase {
249 client_hello: vec![
250 0x3, 0x3, 0x86, 0xad, 0xa4, 0xcc, 0x19, 0xe7, 0x14, 0x54, 0x54, 0xfd, 0xe7,
251 0x37, 0x33, 0xdf, 0x66, 0xcb, 0xf6, 0xef, 0x3e, 0xc0, 0xa1, 0x54, 0xc6, 0xdd,
252 0x14, 0x5e, 0xc0, 0x83, 0xac, 0xb9, 0xb4, 0xe7, 0x20, 0x1c, 0x64, 0xae, 0xa7,
253 0xa2, 0xc3, 0xe1, 0x8c, 0xd1, 0x25, 0x2, 0x4d, 0xf7, 0x86, 0x4a, 0xc7, 0x19,
254 0xd0, 0xc4, 0xbd, 0xfb, 0x40, 0xc2, 0xef, 0x7f, 0x6d, 0xd3, 0x9a, 0xa7, 0x53,
255 0xdf, 0xdd, 0x0, 0x22, 0x1a, 0x1a, 0x13, 0x1, 0x13, 0x2, 0x13, 0x3, 0xc0, 0x2b,
256 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x13, 0xc0,
257 0x14, 0x0, 0x9c, 0x0, 0x9d, 0x0, 0x2f, 0x0, 0x35, 0x0, 0xa, 0x1, 0x0, 0x1,
258 0x91, 0xa, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x1e, 0x0, 0x0, 0x1b, 0x67,
259 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x64, 0x73, 0x2e, 0x67, 0x2e, 0x64, 0x6f,
260 0x75, 0x62, 0x6c, 0x65, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x2e, 0x6e, 0x65, 0x74,
261 0x0, 0x17, 0x0, 0x0, 0xff, 0x1, 0x0, 0x1, 0x0, 0x0, 0xa, 0x0, 0xa, 0x0, 0x8,
262 0x9a, 0x9a, 0x0, 0x1d, 0x0, 0x17, 0x0, 0x18, 0x0, 0xb, 0x0, 0x2, 0x1, 0x0, 0x0,
263 0x23, 0x0, 0x0, 0x0, 0x10, 0x0, 0xe, 0x0, 0xc, 0x2, 0x68, 0x32, 0x8, 0x68,
264 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x0, 0x5, 0x0, 0x5, 0x1, 0x0, 0x0,
265 0x0, 0x0, 0x0, 0xd, 0x0, 0x14, 0x0, 0x12, 0x4, 0x3, 0x8, 0x4, 0x4, 0x1, 0x5,
266 0x3, 0x8, 0x5, 0x5, 0x1, 0x8, 0x6, 0x6, 0x1, 0x2, 0x1, 0x0, 0x12, 0x0, 0x0,
267 0x0, 0x33, 0x0, 0x2b, 0x0, 0x29, 0x9a, 0x9a, 0x0, 0x1, 0x0, 0x0, 0x1d, 0x0,
268 0x20, 0x59, 0x8, 0x6f, 0x41, 0x9a, 0xa5, 0xaa, 0x1d, 0x81, 0xe3, 0x47, 0xf0,
269 0x25, 0x5f, 0x92, 0x7, 0xfc, 0x4b, 0x13, 0x74, 0x51, 0x46, 0x98, 0x8, 0x74,
270 0x3b, 0xde, 0x57, 0x86, 0xe8, 0x2c, 0x74, 0x0, 0x2d, 0x0, 0x2, 0x1, 0x1, 0x0,
271 0x2b, 0x0, 0xb, 0xa, 0xfa, 0xfa, 0x3, 0x4, 0x3, 0x3, 0x3, 0x2, 0x3, 0x1, 0x0,
272 0x1b, 0x0, 0x3, 0x2, 0x0, 0x2, 0xba, 0xba, 0x0, 0x1, 0x0, 0x0, 0x15, 0x0, 0xbd,
273 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
274 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
275 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
276 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
277 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
278 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
279 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
280 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
281 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
282 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
283 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
284 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
285 ],
286 pcap: "chrome-grease-single.pcap",
287 expected_ja3_str: "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53-10,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0",
288 expected_ja3_hash: "66918128f1b9b03303d77c6f2eefd128",
289 },
290 TestCase {
291 client_hello: vec![
292 0x3, 0x3, 0xf6, 0x65, 0xb, 0x22, 0x13, 0xf1, 0xc3, 0xe9, 0xe7, 0xb3, 0xdc, 0x9,
293 0xe4, 0x4b, 0xcb, 0x6e, 0x5, 0xaf, 0x8f, 0x2f, 0x41, 0x8d, 0x15, 0xa8, 0x88,
294 0x46, 0x24, 0x83, 0xca, 0x9, 0x7c, 0x95, 0x20, 0x12, 0xc4, 0x5e, 0x71, 0x8b,
295 0xb9, 0xc9, 0xa9, 0x37, 0x93, 0x4c, 0x41, 0xa6, 0xe8, 0x9e, 0x8f, 0x15, 0x78,
296 0x52, 0xe, 0x3c, 0x28, 0xba, 0xab, 0xa3, 0x34, 0x8b, 0x53, 0x82, 0x83, 0x75,
297 0x24, 0x0, 0x3e, 0x13, 0x2, 0x13, 0x3, 0x13, 0x1, 0xc0, 0x2c, 0xc0, 0x30, 0x0,
298 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x0, 0x9e,
299 0xc0, 0x24, 0xc0, 0x28, 0x0, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x0, 0x67, 0xc0,
300 0xa, 0xc0, 0x14, 0x0, 0x39, 0xc0, 0x9, 0xc0, 0x13, 0x0, 0x33, 0x0, 0x9d, 0x0,
301 0x9c, 0x0, 0x3d, 0x0, 0x3c, 0x0, 0x35, 0x0, 0x2f, 0x0, 0xff, 0x1, 0x0, 0x1,
302 0x75, 0x0, 0x0, 0x0, 0x10, 0x0, 0xe, 0x0, 0x0, 0xb, 0x65, 0x78, 0x61, 0x6d,
303 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x0, 0xb, 0x0, 0x4, 0x3, 0x0, 0x1,
304 0x2, 0x0, 0xa, 0x0, 0xc, 0x0, 0xa, 0x0, 0x1d, 0x0, 0x17, 0x0, 0x1e, 0x0, 0x19,
305 0x0, 0x18, 0x33, 0x74, 0x0, 0x0, 0x0, 0x10, 0x0, 0xe, 0x0, 0xc, 0x2, 0x68,
306 0x32, 0x8, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x0, 0x16, 0x0, 0x0,
307 0x0, 0x17, 0x0, 0x0, 0x0, 0xd, 0x0, 0x30, 0x0, 0x2e, 0x4, 0x3, 0x5, 0x3, 0x6,
308 0x3, 0x8, 0x7, 0x8, 0x8, 0x8, 0x9, 0x8, 0xa, 0x8, 0xb, 0x8, 0x4, 0x8, 0x5, 0x8,
309 0x6, 0x4, 0x1, 0x5, 0x1, 0x6, 0x1, 0x3, 0x3, 0x2, 0x3, 0x3, 0x1, 0x2, 0x1, 0x3,
310 0x2, 0x2, 0x2, 0x4, 0x2, 0x5, 0x2, 0x6, 0x2, 0x0, 0x2b, 0x0, 0x9, 0x8, 0x3,
311 0x4, 0x3, 0x3, 0x3, 0x2, 0x3, 0x1, 0x0, 0x2d, 0x0, 0x2, 0x1, 0x1, 0x0, 0x33,
312 0x0, 0x26, 0x0, 0x24, 0x0, 0x1d, 0x0, 0x20, 0x37, 0x98, 0x48, 0x7f, 0x2f, 0xbc,
313 0x86, 0xf9, 0xb8, 0x2, 0xcd, 0x31, 0xf0, 0x4, 0x30, 0xa9, 0x2f, 0x29, 0x61,
314 0xac, 0xec, 0xc9, 0x2f, 0xf7, 0x45, 0xad, 0xd9, 0x67, 0x7, 0x14, 0x62, 0x1,
315 0x0, 0x15, 0x0, 0xb6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
316 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
317 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
318 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
319 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
320 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
321 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
322 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
323 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
324 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
325 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
326 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
327 ],
328 pcap: "curl.pcap",
329 expected_ja3_str: "771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-13172-16-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2",
330 expected_ja3_hash: "456523fc94726331a4d5a2e1d40b2cd7",
331 },
332 ];
333 for test_case in test_cases {
334 let mut ext = Extensions::new();
335 ext.insert(SecureTransport::with_client_hello(
336 parse_client_hello(&test_case.client_hello).expect(test_case.pcap),
337 ));
338
339 let ja3 = Ja3::compute(&ext).expect(test_case.pcap);
340
341 assert_eq!(
342 test_case.expected_ja3_str,
343 format!("{ja3}"),
344 "pcap: {}",
345 test_case.pcap,
346 );
347
348 assert_eq!(
349 test_case.expected_ja3_hash,
350 format!("{ja3:x}"),
351 "pcap: {}",
352 test_case.pcap,
353 );
354 }
355 }
356}