1use rustls::{
32 internal::msgs::{
33 enums::{ECPointFormat, ExtensionType, NamedGroup},
34 handshake::ClientExtension,
35 },
36 CipherSuite, ProtocolVersion,
37};
38
39pub use rustls::internal::msgs::{handshake::ClientHelloPayload, message::Message};
40
41use std::{fmt, str::FromStr};
42
43mod grease;
44mod ja3nm;
45mod utils;
46
47use crate::utils::{fmtconcat, get_client_tls_versions, ConcatenatedParser};
48pub use crate::utils::{parse_tls_plain_message, TlsMessageExt};
49
50use crate::ja3nm::get_ja3_and_more_from_chp;
51pub use crate::ja3nm::Ja3andMore;
52
53pub use crate::grease::*;
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Ja3 {
58 pub version: u16,
60 pub ciphers: Vec<u16>,
62 pub extensions: Vec<u16>,
64 pub curves: Vec<u16>,
66 pub point_formats: Vec<u8>,
68}
69
70pub trait Ja3Extractor {
71 fn ja3(&self) -> Ja3;
73
74 fn ja3_with_real_version(&self) -> Ja3;
79
80 fn ja3_with_real_version_and_grease(&self) -> Ja3;
82
83 fn ja3_with_grease(&self) -> Ja3;
88
89 fn ja3_and_more(&self) -> Ja3andMore;
93
94 fn ja3_and_more_with_grease(&self) -> Ja3andMore;
99}
100
101impl Ja3Extractor for ClientHelloPayload {
102 fn ja3(&self) -> Ja3 {
103 get_ja3_from_chp(self, false, true)
104 }
105
106 fn ja3_with_real_version(&self) -> Ja3 {
107 get_ja3_from_chp(self, true, true)
108 }
109
110 fn ja3_with_real_version_and_grease(&self) -> Ja3 {
111 get_ja3_from_chp(self, true, true)
112 }
113
114 fn ja3_with_grease(&self) -> Ja3 {
115 get_ja3_from_chp(self, true, false)
116 }
117
118 fn ja3_and_more(&self) -> Ja3andMore {
119 get_ja3_and_more_from_chp(self, true)
120 }
121
122 fn ja3_and_more_with_grease(&self) -> Ja3andMore {
123 get_ja3_and_more_from_chp(self, false)
124 }
125}
126
127fn get_ja3_from_chp(
128 chp: &ClientHelloPayload,
129 use_real_version: bool,
130 ignore_rfc8701_grease: bool,
131) -> Ja3 {
132 let mut version = chp.client_version;
134 if use_real_version
135 && get_client_tls_versions(chp)
136 .map(|vers| vers.iter().any(|&ver| ver == ProtocolVersion::TLSv1_3))
137 .unwrap_or(false)
138 {
139 version = ProtocolVersion::TLSv1_3;
141 }
142 let ciphers = chp
143 .cipher_suites
144 .iter()
145 .map(|cipher| cipher.get_u16())
146 .filter(|&ext| !ignore_rfc8701_grease || !is_grease_u16_be(ext))
147 .collect();
148 let extensions = chp
149 .extensions
150 .iter()
151 .map(|extension| extension.get_type().get_u16())
152 .filter(|&ext| !ignore_rfc8701_grease || !is_grease_u16_be(ext))
153 .collect();
154
155 let mut curves = Vec::<u16>::new();
156 let mut point_formats = Vec::<u8>::new();
157 for extension in chp.extensions.iter() {
158 match extension {
159 ClientExtension::NamedGroups(groups) => {
160 curves = groups
161 .iter()
162 .map(|curve| curve.get_u16())
163 .filter(|&ext| !ignore_rfc8701_grease || !is_grease_u16_be(ext))
164 .collect()
165 }
166 ClientExtension::ECPointFormats(formats) => {
167 point_formats = formats.iter().map(|format| format.get_u8()).collect()
168 }
169 _ => {}
170 }
171 }
172 Ja3 {
173 version: version.get_u16(),
174 ciphers,
175 extensions,
176 curves,
177 point_formats,
178 }
179}
180
181impl From<&ClientHelloPayload> for Ja3 {
182 #[inline(always)]
183 fn from(chp: &ClientHelloPayload) -> Ja3 {
184 chp.ja3()
185 }
186}
187
188impl Ja3 {
189 pub fn version_to_typed(&self) -> ProtocolVersion {
190 ProtocolVersion::from(self.version)
191 }
192
193 pub fn ciphers_as_typed(&self) -> impl Iterator<Item = CipherSuite> + '_ {
194 self.ciphers.iter().map(|&cipher| CipherSuite::from(cipher))
195 }
196
197 pub fn ciphers_regreasing_as_typed(&self) -> impl Iterator<Item = CipherSuite> + '_ {
200 self.ciphers
201 .iter()
202 .map(|&cipher| CipherSuite::from(try_regrease_u16_be(cipher)))
203 }
204
205 pub fn extensions_as_typed(&self) -> impl Iterator<Item = ExtensionType> + '_ {
206 self.extensions
207 .iter()
208 .map(|&extension| ExtensionType::from(extension))
209 }
210
211 pub fn extensions_regreasing_as_typed(&self) -> impl Iterator<Item = ExtensionType> + '_ {
214 self.extensions
215 .iter()
216 .map(|&extension| ExtensionType::from(try_regrease_u16_be(extension)))
217 }
218
219 pub fn curves_as_typed(&self) -> impl Iterator<Item = NamedGroup> + '_ {
220 self.curves.iter().map(|&curve| NamedGroup::from(curve))
221 }
222
223 pub fn curves_regreasing_as_typed(&self) -> impl Iterator<Item = NamedGroup> + '_ {
226 self.curves
227 .iter()
228 .map(|&curve| NamedGroup::from(try_regrease_u16_be(curve)))
229 }
230 pub fn point_formats_as_typed(&self) -> impl Iterator<Item = ECPointFormat> + '_ {
231 self.point_formats
232 .iter()
233 .map(|&format| ECPointFormat::from(format))
234 }
235}
236
237impl fmt::Display for Ja3 {
238 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
239 let Self {
240 version,
241 ciphers,
242 extensions,
243 curves,
244 point_formats,
245 } = self;
246 write!(
247 f,
248 "{},{},{},{},{}",
249 version,
250 fmtconcat::<_, '-'>(ciphers),
251 fmtconcat::<_, '-'>(extensions),
252 fmtconcat::<_, '-'>(curves),
253 fmtconcat::<_, '-'>(point_formats)
254 )?;
255 Ok(())
256 }
257}
258
259impl FromStr for Ja3 {
260 type Err = &'static str; fn from_str(s: &str) -> Result<Ja3, Self::Err> {
263 let mut parts = s.split(',');
264 let version = parts
265 .next()
266 .ok_or("Emtpy string")?
267 .parse::<u16>()
268 .map_err(|_e| "Version not integer")?;
269 let ciphers = parts
270 .next()
271 .ok_or("No ciphers and following")?
272 .parse::<ConcatenatedParser<_, '-'>>()?
273 .into_inner();
274 let extensions = parts
275 .next()
276 .ok_or("No extensiosn and following")?
277 .parse::<ConcatenatedParser<_, '-'>>()?
278 .into_inner();
279 let curves = parts
280 .next()
281 .ok_or("No curves and following")?
282 .parse::<ConcatenatedParser<_, '-'>>()?
283 .into_inner();
284 let point_formats = parts
285 .next()
286 .ok_or("No point formats")?
287 .parse::<ConcatenatedParser<_, '-'>>()?
288 .into_inner();
289 if parts.next().is_some() {
290 return Err("String redundant");
291 }
292 Ok(Ja3 {
293 version,
294 ciphers,
295 extensions,
296 curves,
297 point_formats,
298 })
299 }
300}
301
302#[cfg(feature = "md5")]
303impl Ja3 {
304 pub fn to_md5(&self) -> [u8; 16] {
306 use md5::{Digest, Md5};
307 let mut h = Md5::new();
308 h.update(self.to_string().as_bytes());
309 h.finalize().as_slice().try_into().unwrap()
310 }
311
312 #[cfg(feature = "md5-string")]
313 #[inline(always)]
314 pub fn to_md5_string(&self) -> String {
316 hex::encode(self.to_md5())
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use hex_literal::hex;
323
324 use crate::utils::{parse_tls_plain_message, TlsMessageExt};
325 use crate::{Ja3, Ja3Extractor};
326 #[test]
327 fn ja3_from_client_hello_message() {
328 let buf = hex!("16030100f5010000f10303ad8e0c8dfe3adbc045e51aee4cb9480c02d5da4a240f95e8282a1f51be34901a20681af80b44c4b359adb3f9543a966e07e6ba6bed551472a62cd4b107cbd40e830014130213011303c02cc02bcca9c030c02fcca800ff01000094002b00050403040303000b00020100000a00080006001d00170018000d00140012050304030807080608050804060105010401001700000005000501000000000000001800160000137777772e706574616c7365617263682e636f6d00120000003300260024001d0020086fffef5fa7f04fb7d788615bc425820eba366ddb5f75c7d8336a0a05722d38002d0002010100230000");
330 let chp = parse_tls_plain_message(&buf)
331 .ok()
332 .and_then(|message| message.into_client_hello_payload())
333 .expect("Message valid");
334 println!("{:?}", chp.ja3());
335 println!("{}", chp.ja3());
336 println!("{}", chp.ja3_with_real_version());
337 assert_eq!(chp.ja3().to_string(), "771,4866-4865-4867-49196-49195-52393-49200-49199-52392-255,43-11-10-13-23-5-0-18-51-45-35,29-23-24,0");
338 assert_eq!(chp.ja3_with_real_version().to_string(), "772,4866-4865-4867-49196-49195-52393-49200-49199-52392-255,43-11-10-13-23-5-0-18-51-45-35,29-23-24,0");
339 #[cfg(feature = "md5-string")]
340 {
341 assert_eq!(
342 chp.ja3().to_md5_string(),
343 "a94fc11547bcef10847672ff518b3fb9"
344 )
345 }
346 }
347
348 #[test]
349 fn ja3_from_string() {
350 let buf = hex!("16030100f5010000f10303ad8e0c8dfe3adbc045e51aee4cb9480c02d5da4a240f95e8282a1f51be34901a20681af80b44c4b359adb3f9543a966e07e6ba6bed551472a62cd4b107cbd40e830014130213011303c02cc02bcca9c030c02fcca800ff01000094002b00050403040303000b00020100000a00080006001d00170018000d00140012050304030807080608050804060105010401001700000005000501000000000000001800160000137777772e706574616c7365617263682e636f6d00120000003300260024001d0020086fffef5fa7f04fb7d788615bc425820eba366ddb5f75c7d8336a0a05722d38002d0002010100230000");
351 let chp = parse_tls_plain_message(&buf)
352 .ok()
353 .and_then(|message| message.into_client_hello_payload())
354 .expect("Message valid");
355 let ja3 = "771,4866-4865-4867-49196-49195-52393-49200-49199-52392-255,43-11-10-13-23-5-0-18-51-45-35,29-23-24,0".parse().unwrap();
356 println!("{:?}", ja3);
357 assert_eq!(chp.ja3(), ja3);
358 assert!("771,4866-4865-4867-49196-49195-52393-49200-49199-52392-255,43-11-10-13-23-5-0-18-51-45-35,29-23-24,0,".parse::<Ja3>().is_err());
359 assert!("771,".parse::<Ja3>().is_err());
360 assert!("a,4866-4865-4867-49196-49195-52393-49200-49199-52392-255,43-11-10-13-23-5-0-18-51-45-35,29-23-24,0".parse::<Ja3>().is_err());
361 }
362
363 #[test]
364 fn regreasing() {
365 let ja3:Ja3 = "771,2570-4866-4865-4867-49196-49195-52393-49200-49199-52392-255,2570-43-11-10-13-23-5-0-18-51-45-2570-35,2570-29-23-24,0".parse().unwrap();
366 assert_ne!(
368 ja3.ciphers_regreasing_as_typed().collect::<Vec<_>>(),
369 ja3.ciphers_regreasing_as_typed().collect::<Vec<_>>()
370 );
371 assert_ne!(
372 ja3.extensions_regreasing_as_typed().collect::<Vec<_>>(),
373 ja3.extensions_regreasing_as_typed().collect::<Vec<_>>()
374 );
375 assert_ne!(
376 ja3.curves_regreasing_as_typed().collect::<Vec<_>>(),
377 ja3.curves_regreasing_as_typed().collect::<Vec<_>>()
378 );
379 assert_eq!(
380 ja3.ciphers_as_typed().collect::<Vec<_>>(),
381 ja3.ciphers_as_typed().collect::<Vec<_>>()
382 );
383 assert_eq!(
384 ja3.extensions_as_typed().collect::<Vec<_>>(),
385 ja3.extensions_as_typed().collect::<Vec<_>>()
386 );
387 assert_eq!(
388 ja3.curves_as_typed().collect::<Vec<_>>(),
389 ja3.curves_as_typed().collect::<Vec<_>>()
390 );
391 }
392}