ja3_rustls/
lib.rs

1//! JA3 with Rustls types
2//!
3//! # Example
4//! Extract JA3 TLS fingerprint from a slice of bytes:
5//! ```rust
6//! use ja3_rustls::{parse_tls_plain_message, TlsMessageExt, Ja3Extractor};
7//! use hex_literal::hex;
8//!
9//! let buf = hex!("16030100f5010000f10303ad8e0c8dfe3adbc045e51aee4cb9480c02d5da4a240f95e8282a1f51be34901a20681af80b44c4b359adb3f9543a966e07e6ba6bed551472a62cd4b107cbd40e830014130213011303c02cc02bcca9c030c02fcca800ff01000094002b00050403040303000b00020100000a00080006001d00170018000d00140012050304030807080608050804060105010401001700000005000501000000000000001800160000137777772e706574616c7365617263682e636f6d00120000003300260024001d0020086fffef5fa7f04fb7d788615bc425820eba366ddb5f75c7d8336a0a05722d38002d0002010100230000");
10//! let chp = parse_tls_plain_message(&buf)
11//!   .ok()
12//!   .and_then(|message| message.into_client_hello_payload())
13//!   .expect("Message valid");
14//! println!("{:?}", chp.ja3());
15//! println!("{}", chp.ja3());
16//! println!("{}", chp.ja3_with_real_version());
17//! 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");
18//! ```
19//!
20//! To generate hex string of JA3, activating optional features via Cargo.toml:
21//! ```toml
22//! # in Cargo.toml
23//! # under [dependencies]
24//! ja3-rustls = { version = "0.0.0", features = ["md5-string"] } # or just md5
25//! ```
26//! , then
27//! ```ignore
28//! println!("{:x?}", chp.ja3().to_md5()); // requires feature: md5
29//! println!("{}", chp.ja3().to_md5_string()); // requires feature: md5-string
30//! ```
31use 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)]
56/// JA3, as explained in [https://github.com/salesforce/ja3]
57pub struct Ja3 {
58    /// SSLVersion
59    pub version: u16,
60    /// Cipher
61    pub ciphers: Vec<u16>,
62    /// SSLExtension
63    pub extensions: Vec<u16>,
64    /// EllipticCurve
65    pub curves: Vec<u16>,
66    /// EllipticCurvePointFormat
67    pub point_formats: Vec<u8>,
68}
69
70pub trait Ja3Extractor {
71    /// Get the JA3 of a [`ClientHelloPayload`]
72    fn ja3(&self) -> Ja3;
73
74    /// Almost same as `ja3`, except that the TLS version specified in extensions, if any, are
75    /// preferred over the one indicated by the record header.
76    ///
77    /// This appears to be imcompliant to the JA3 standard.
78    fn ja3_with_real_version(&self) -> Ja3;
79
80    /// Check `ja3_with_real_version` and `ja3_with_grease` for more info.
81    fn ja3_with_real_version_and_grease(&self) -> Ja3;
82
83    /// Almost same `ja3`, except that all [RFC8701](https://www.rfc-editor.org/rfc/rfc8701.html)
84    /// GREASE values are kept as is.
85    ///
86    /// This contradicts the JA3 standard.
87    fn ja3_with_grease(&self) -> Ja3;
88
89    /// Get the JA3 and more fields of a [`ClientHelloPayload`]
90    ///
91    /// Additional fields are not a part of the JA3 standard.
92    fn ja3_and_more(&self) -> Ja3andMore;
93
94    /// Almost same `ja3_and_more`, except that all [RFC8701](https://www.rfc-editor.org/rfc/rfc8701.html)
95    /// GREASE values are kept as is.
96    ///
97    /// This contradicts the JA3 standard.
98    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    // SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
133    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        // TODO: only TLS 1.3 is considered for now
140        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    /// `ciphers_as_typed` with existing GREASE values rewritten as newly generated ones. It
198    /// is based on an insecure RNG unless the `rand` crate feature is activated.
199    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    /// `extensions_as_typed` with existing GREASE values rewritten as newly generated ones. It
212    /// is based on an insecure RNG unless the `rand` crate feature is activated.
213    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    /// `curves_as_typed` with existing GREASE values rewritten as newly generated ones. It
224    /// is based on an insecure RNG unless the `rand` crate feature is activated.
225    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; // TODO: typing
261
262    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    /// Helper function to generate the MD5 format of JA3 string.
305    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    /// Helper function to generate the MD5 (hex string) format of JA3 string.
315    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        // rustls safe default client ja3
329        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        // failed chance: (1 - (1 - 15/16) ** 3) ~= 17.6%
367        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}