ja3_new/
lib.rs

1//! JA3 Hash
2//!
3//! A small TLS fingerprinting library written in Rust.
4//!
5//! This crate enables a consumer to fingerprint the ClientHello portion of a TLS handshake.
6//! It can hash TLS handshakes over IPv4 and IPv6. It heavily depends on the [tls-parser
7//! project](https://github.com/rusticata/tls-parser) from Rusticata.
8//!
9//! It supports generating fingerprints from packet capture files as well as live-captures
10//! on a network interface, both using libpcap.
11//!
12//! See the original [JA3 project](https://github.com/salesforce/ja3) for more information.
13//!
14//! Example of fingerprinting a packet capture file:
15//!
16//! ```rust,no_run
17//! use ja3::Ja3;
18//!
19//! let mut ja3 = Ja3::new("test.pcap")
20//!                     .process_pcap()
21//!                     .unwrap();
22//!
23//! // Now we have a Vec of Ja3Hash objects
24//! for hash in ja3 {
25//!     println!("{}", hash);
26//! }
27//! ```
28//!
29//! Example of fingerprinting a live capture:
30//!
31//! ```rust,ignore
32//! use ja3::Ja3;
33//!
34//! let mut ja3 = Ja3::new("eth0")
35//!                     .process_live()
36//!                     .unwrap();
37//! while let Some(hash) = ja3.next() {
38//!     println!("{}", hash);
39//! }
40//!
41//! ```
42
43use std::fs::File;
44use std::ffi::{OsStr, OsString};
45use std::fmt;
46use std::net::IpAddr;
47
48use lazy_static::*;
49use log::{info, debug};
50use md5::{self, Digest};
51#[cfg(feature = "live-capture")]
52use pcap::{Active, Capture};
53use pcap_parser::{LegacyPcapReader, PcapBlockOwned, PcapError};
54use pcap_parser::traits::PcapReaderIterator;
55use pnet::packet::ethernet::EtherType;
56use pnet::packet::ip::IpNextHeaderProtocol;
57use pnet::packet::ip::IpNextHeaderProtocols;
58use pnet::packet::*;
59use tls_parser::parse_tls_plaintext;
60use tls_parser::tls::{TlsMessage, TlsMessageHandshake, TlsRecordType};
61use tls_parser::tls_extensions::{parse_tls_extensions, TlsExtension, TlsExtensionType};
62
63mod errors;
64use errors::*;
65use failure::Error;
66
67lazy_static! {
68    static ref IPTYPE: IpNextHeaderProtocol = IpNextHeaderProtocol::new(6);
69    static ref GREASE: Vec<u16> = vec![
70        0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa,
71        0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa
72    ];
73}
74
75/// A JA3 hash builder. This provides options about how to extract a JA3 hash from a TLS handshake.
76#[derive(Debug)]
77pub struct Ja3 {
78    i: Ja3Inner,
79}
80
81// TODO: add support for RAW captures
82#[derive(Debug)]
83struct Ja3Inner {
84    path: OsString,
85    tls_port: u16,
86}
87
88/// The output of a JA3 hash object. This consists of the JA3 string and MD5 hash.
89#[derive(Debug, Eq)]
90pub struct Ja3Hash {
91    /// The string consisting of the SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
92    /// See the original [JA3 specification](https://github.com/salesforce/ja3#how-it-works) for more info.
93    pub ja3_str: String,
94    /// The MD5 hash of `ja3_str`.
95    pub hash: Digest,
96    /// The destination IP address of the TLS handshake.
97    pub source: IpAddr,
98    /// The source IP address of the TLS handshake.
99    pub destination: IpAddr,
100}
101
102/// Iterator of JA3 hashes captured during a live capture.
103#[cfg(feature = "live-capture")]
104pub struct Ja3Live {
105    cap: Capture<Active>,
106    ja3_inner: Ja3,
107}
108
109#[cfg(feature = "live-capture")]
110impl Iterator for Ja3Live {
111    type Item = Ja3Hash;
112
113    fn next(&mut self) -> Option<Self::Item> {
114        while let Ok(packet) = self.cap.next() {
115            match self.ja3_inner.process_packet_common(&packet) {
116                Ok(s) => return Some(s),
117                Err(_) => continue,
118            };
119        }
120
121        None
122    }
123}
124
125impl Ja3 {
126    /// Creates a new Ja3 object.
127    ///
128    /// It will extract JA3 hashes from the packet capture located at `pcap_path` or
129    /// the network interface named `pcap_path`, depending on whether the consumer calls
130    /// `process_pcap` or `process_live`.
131    pub fn new<S: AsRef<OsStr>>(pcap_path: S) -> Self {
132        let mut path = OsString::new();
133        path.push(pcap_path);
134        let i = Ja3Inner {
135            path: path,
136            tls_port: 443,
137        };
138
139        Ja3 { i: i }
140    }
141
142    /// Change the hasher behavior to scan for TLS handshakes occuring on *any* TCP port. By
143    /// default we only fingerprint handshakes on TCP 443.
144    pub fn any_port<'a>(&'a mut self) -> &'a mut Self {
145        self.i.tls_port = 0;
146        self
147    }
148
149    /// Scans the provided packet capture for TLS handshakes and returns JA3 hashes for any found.
150    pub fn process_pcap(&self) -> Result<Vec<Ja3Hash>, Error> {
151        let mut results: Vec<Ja3Hash> = Vec::new();
152
153        let file = File::open(&self.i.path)?;
154        let mut reader = LegacyPcapReader::new(65536, file).expect("LegacyPcapReader");
155        loop {
156            match reader.next() {
157                Ok((offset, block)) => {
158                    match block {
159                        PcapBlockOwned::LegacyHeader(_hdr) => {
160                            // save hdr.network (linktype)
161                        },
162                        PcapBlockOwned::Legacy(block) => {
163                            let ja3_hash = match self.process_packet_common(&block.data) {
164                                Ok(s) => s,
165                                Err(_) => {
166                                    reader.consume(offset);
167                                    continue;
168                                },
169                            };
170                            debug!("Adding JA3: {:?}", ja3_hash);
171                            results.push(ja3_hash);
172                        },
173                        PcapBlockOwned::NG(_) => unreachable!(),
174                    }
175                    reader.consume(offset);
176                },
177                Err(PcapError::Eof) => break,
178                Err(PcapError::Incomplete) => {
179                    reader.refill().unwrap();
180                },
181                Err(e) => return Err(e.into()),
182            }
183        }
184
185        Ok(results)
186    }
187
188    /// Opens a live packet capture and scans packets for TLS handshakes and returns an iterator of
189    /// JA3 hashes found.
190    #[cfg(feature = "live-capture")]
191    pub fn process_live(self) -> Result<Ja3Live, Error> {
192        let cap = Capture::from_device(self.i.path.to_str().unwrap())?.open()?;
193        info!("cap: {:?}", self.i.path);
194        //while let Ok(packet) = cap.next() {
195        //    let ja3_hash = match self.process_packet_common(&packet) {
196        //        Ok(s) => s,
197        //        Err(_) => continue,
198        //    };
199
200        //    info!("Calling callback with JA3: {:?}", ja3_hash);
201        //    cb(&ja3_hash);
202        //}
203
204        Ok(Ja3Live {
205            cap: cap,
206            ja3_inner: self,
207        })
208    }
209
210    fn process_packet_common(&self, packet: &[u8]) -> Result<Ja3Hash, Error> {
211        let saddr;
212        let daddr;
213        let ether = ethernet::EthernetPacket::new(&packet).ok_or(Ja3Error::ParseError)?;
214        info!("\nether packet: {:?} len: {}", ether, ether.packet_size());
215        let tcp_start = match ether.get_ethertype() {
216            EtherType(0x0800) => {
217                let ip = ipv4::Ipv4Packet::new(&packet[ether.packet_size()..])
218                    .ok_or(Ja3Error::ParseError)?;
219                info!("\nipv4 packet: {:?}", ip);
220                if ip.get_next_level_protocol() != *IPTYPE {
221                    return Err(Ja3Error::ParseError)?;
222                }
223                let iphl = ip.get_header_length() as usize * 4;
224                saddr = IpAddr::V4(ip.get_source());
225                daddr = IpAddr::V4(ip.get_destination());
226                iphl + ether.packet_size()
227            }
228            EtherType(0x86dd) => {
229                let ip = ipv6::Ipv6Packet::new(&packet[ether.packet_size()..])
230                    .ok_or(Ja3Error::ParseError)?;
231                info!("\nipv6 packet: {:?}", ip);
232                saddr = IpAddr::V6(ip.get_source());
233                daddr = IpAddr::V6(ip.get_destination());
234                if ip.get_next_header() != IpNextHeaderProtocols::Tcp {
235                    return Err(Ja3Error::NotHandshake)?;
236                }
237                let iphl = 40;
238                iphl + ether.packet_size()
239            }
240            _ => return Err(Ja3Error::ParseError)?,
241        };
242
243        let tcp = tcp::TcpPacket::new(&packet[tcp_start..]).ok_or(Ja3Error::ParseError)?;
244        info!("tcp: {:?}", tcp);
245        if self.i.tls_port != 0 {
246            if tcp.get_destination() != 443 {
247                return Err(Ja3Error::NotHandshake)?;
248            }
249        }
250
251        info!("pack size: {}", tcp.packet_size());
252        let handshake_start = tcp_start + tcp.packet_size();
253        info!("handshake_start: {}", handshake_start);
254        let handshake = &packet[handshake_start..];
255        if handshake.len() <= 0 {
256            return Err(Ja3Error::NotHandshake)?;
257        }
258        if handshake[0] != 0x16 {
259            return Err(Ja3Error::NotHandshake)?;
260        }
261        info!("handshake: {:x?}", handshake);
262
263        info!("sending handshake {:?}", handshake);
264        let ja3_string = self.ja3_string_client_hello(&handshake).unwrap();
265        if ja3_string == "" {
266            return Err(Ja3Error::NotHandshake)?;
267        }
268
269        let hash = md5::compute(&ja3_string.as_bytes());
270        let ja3_res = Ja3Hash {
271            ja3_str: ja3_string,
272            hash: hash,
273            source: saddr,
274            destination: daddr,
275        };
276
277        Ok(ja3_res)
278    }
279
280    fn process_extensions(&self, extensions: &[u8]) -> Option<String> {
281        let mut ja3_exts = String::new();
282        let mut supported_groups = String::new();
283        let mut ec_points = String::new();
284        let (_, exts) = parse_tls_extensions(extensions).unwrap();
285        for extension in exts {
286            let ext_val = u16::from(TlsExtensionType::from(&extension));
287            if GREASE.contains(&ext_val) {
288                continue;
289            }
290            info!("Ext: {:?}", ext_val);
291            ja3_exts.push_str(&format!("{}-", ext_val));
292            match extension {
293                TlsExtension::EllipticCurves(curves) => {
294                    for curve in curves {
295                        if !GREASE.contains(&curve.0) {
296                            info!("curve: {}", curve.0);
297                            supported_groups.push_str(&format!("{}-", curve.0));
298                        }
299                    }
300                }
301                TlsExtension::EcPointFormats(points) => {
302                    info!("Points: {:x?}", points);
303                    for point in points {
304                        ec_points.push_str(&format!("{}-", point));
305                    }
306                }
307                _ => {}
308            }
309        }
310        ja3_exts.pop();
311        supported_groups.pop();
312        ec_points.pop();
313        info!("Supported groups: {}", supported_groups);
314        info!("EC Points: {}", ec_points);
315        let ret = format!("{},{},{}", ja3_exts, supported_groups, ec_points);
316        Some(ret)
317    }
318
319    fn ja3_string_client_hello(&self, packet: &[u8]) -> Option<String> {
320        info!("PACKET: {:?}", packet);
321        let mut ja3_string = String::new();
322        let res = parse_tls_plaintext(packet);
323        match res {
324            Ok((rem, record)) => {
325                info!("Rem: {:?}, record: {:?}", rem, record);
326                info!("record type: {:?}", record.hdr.record_type);
327                if record.hdr.record_type != TlsRecordType::Handshake {
328                    return None;
329                }
330                for rec in record.msg {
331                    if let TlsMessage::Handshake(handshake) = rec {
332                        if let TlsMessageHandshake::ClientHello(contents) = handshake {
333                            info!("handshake contents: {:?}", contents);
334                            info!("handshake tls version: {:?}", u16::from(contents.version));
335                            ja3_string.push_str(&format!("{},", u16::from(contents.version)));
336                            for cipher in contents.ciphers {
337                                info!("handshake cipher: {}", u16::from(cipher));
338                                if !GREASE.contains(&cipher) {
339                                    ja3_string.push_str(&format!("{}-", u16::from(cipher)));
340                                }
341                            }
342                            ja3_string.pop();
343                            ja3_string.push(',');
344                            if let Some(extensions) = contents.ext {
345                                let ext = self.process_extensions(extensions).unwrap();
346                                ja3_string.push_str(&ext);
347                            }
348                        }
349                    }
350                }
351            }
352            _ => {
353                info!("ERROR");
354                return None;
355            }
356        }
357
358        info!("ja3_string: {}", ja3_string);
359        Some(ja3_string)
360    }
361}
362
363impl fmt::Display for Ja3Hash {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        write!(
366            f,
367            "[{} --> {}] {} {:x}",
368            self.source, self.destination, self.ja3_str, self.hash
369        )
370    }
371}
372
373impl PartialEq for Ja3Hash {
374    fn eq(&self, other: &Self) -> bool {
375        self.hash == other.hash
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use env_logger;
383    use nix::unistd::{fork, ForkResult};
384    use pretty_assertions::assert_eq;
385    use rusty_fork::rusty_fork_id;
386    use rusty_fork::rusty_fork_test;
387    use rusty_fork::rusty_fork_test_name;
388    use std::net::{IpAddr, Ipv4Addr};
389    use std::process::Command;
390
391    // NOTE: Any test for the live-capture feature requires elevated privileges.
392
393    #[cfg(feature = "live-capture")]
394    rusty_fork_test! {
395    #[test] #[ignore]
396    fn test_ja3_client_hello_chrome_grease_single_packet_live() {
397        let expected_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";
398        let expected_hash = "66918128f1b9b03303d77c6f2eefd128";
399        let expected_daddr = IpAddr::V6("2607:f8b0:4004:814::2002".parse().unwrap());
400
401        match fork() {
402            Ok(ForkResult::Parent { child: _, .. }) => {
403                let mut ja3 = Ja3::new("lo")
404                                .process_live().unwrap();
405                if let Some(x) = ja3.next() {
406                    assert_eq!(x.ja3_str, expected_str);
407                    assert_eq!(format!("{:x}", x.hash), expected_hash);
408                    assert_eq!(expected_daddr, x.destination);
409                    std::process::exit(0);
410                }
411            },
412            Ok(ForkResult::Child) => {
413                let _out = Command::new("tcpreplay")
414                            .arg("-i")
415                            .arg("lo")
416                            .arg("chrome-grease-single.pcap")
417                            .output()
418                            .expect("failed to execute process");
419            },
420            Err(_) => println!("Fork failed"),
421        }
422
423    }
424    }
425
426    #[test]
427    fn test_ja3_client_hello_chrome_grease_single_packet() {
428        let expected_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";
429        let expected_hash = "66918128f1b9b03303d77c6f2eefd128";
430        let expected_daddr = IpAddr::V6("2607:f8b0:4004:814::2002".parse().unwrap());
431
432        let mut ja3 = Ja3::new("tests/chrome-grease-single.pcap")
433            .process_pcap()
434            .unwrap();
435        let ja3_hash = ja3.pop().unwrap();
436        assert_eq!(ja3_hash.ja3_str, expected_str);
437        assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
438        assert_eq!(expected_daddr, ja3_hash.destination);
439    }
440
441    #[test]
442    fn test_ja3_client_hello_firefox_single_packet() {
443        let expected_str = "771,49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-13-28,29-23-24-25,0";
444        let expected_hash = "839bbe3ed07fed922ded5aaf714d6842";
445        let expected_daddr = IpAddr::V4("34.209.18.179".parse().unwrap());
446
447        let mut ja3 = Ja3::new("tests/test.pcap").process_pcap().unwrap();
448        let ja3_hash = ja3.pop().unwrap();
449        assert_eq!(ja3_hash.ja3_str, expected_str);
450        assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
451        assert_eq!(expected_daddr, ja3_hash.destination);
452    }
453
454    #[test]
455    fn test_ja3_curl_full_stream() {
456        let expected_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";
457        let expected_hash = "456523fc94726331a4d5a2e1d40b2cd7";
458        let expected_daddr = IpAddr::V4("93.184.216.34".parse().unwrap());
459
460        let mut ja3s = Ja3::new("tests/curl.pcap").process_pcap().unwrap();
461        let ja3 = ja3s.pop().unwrap();
462        assert_eq!(ja3.ja3_str, expected_str);
463        assert_eq!(format!("{:x}", ja3.hash), expected_hash);
464        assert_eq!(expected_daddr, ja3.destination);
465    }
466
467    #[test]
468    fn test_ja3_curl_full_stream_ipv6() {
469        let expected_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";
470        let expected_hash = "456523fc94726331a4d5a2e1d40b2cd7";
471        let expected_daddr = IpAddr::V6("2606:2800:220:1:248:1893:25c8:1946".parse().unwrap());
472
473        let mut ja3s = Ja3::new("tests/curl-ipv6.pcap").process_pcap().unwrap();
474        let ja3 = ja3s.pop().unwrap();
475        assert_eq!(ja3.ja3_str, expected_str);
476        assert_eq!(format!("{:x}", ja3.hash), expected_hash);
477        assert_eq!(expected_daddr, ja3.destination);
478    }
479
480    #[test]
481    fn test_ja3_client_hello_ncat_full_stream_non_tls_port() {
482        let expected_str = "771,4866-4867-4865-49196-49200-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-49188-49192-107-106-49267-49271-196-195-49162-49172-57-56-136-135-157-49313-49309-49233-61-192-53-132-49195-49199-162-158-49326-49324-49314-49310-49244-49248-49238-49234-49187-49191-103-64-49266-49270-190-189-49161-49171-51-50-154-153-69-68-156-49312-49308-49232-60-186-47-150-65-255,0-11-10-35-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2";
483        let expected_hash = "10a6b69a81bac09072a536ce9d35dd43";
484
485        let mut ja3 = Ja3::new("tests/ncat-port-4450.pcap")
486            .any_port()
487            .process_pcap()
488            .unwrap();
489        let ja3_hash = ja3.pop().unwrap();
490        assert_eq!(ja3_hash.ja3_str, expected_str);
491        assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
492        assert_eq!(
493            IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
494            ja3_hash.destination
495        );
496    }
497}