rama_net/fingerprint/
ja3.rs

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)]
12/// Data which can be hashed using [`Self::hash`],
13/// and which is also displayed as a "ja3" hash.
14///
15/// Computed using [`Ja3::compute`].
16pub 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    /// Compute the [`Ja3`] (hash).
26    ///
27    /// As specified by <https://github.com/salesforce/ja3`>.
28    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    /// Compute the [`Ja3`] (hash) from a reference to either a
40    /// [`ClientHello`] or a [`ClientConfig`] data structure.
41    ///
42    /// In case your source is [`Extensions`] you can use [`Self::compute`] instead.
43    ///
44    /// [`ClientHello`]: crate::tls::client::ClientHello
45    /// [`ClientConfig`]: crate::tls::client::ClientConfig
46    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    /// compute the "ja3" hash from this [`Ja3`] data structure as a String.
104    pub fn hash(&self) -> String {
105        format!("{self:x}")
106    }
107
108    /// compute the "ja3" hash from this [`Ja3`] data structure into the writer.
109    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)]
207/// error identifying a failure in [`Ja3::compute`]
208pub enum Ja3ComputeError {
209    /// missing [`ClientHello`]
210    MissingClientHello,
211    /// cipher suites was empty
212    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        // src: <https://github.com/jabedude/ja3-rs/blob/a30d1bea03d2230b1239d437c3f6af7fb7699338/src/lib.rs#L380>
247        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}