From db49b5936bd80ddd0e87d359a5e053d468b4aa2b Mon Sep 17 00:00:00 2001 From: "Valeriy V. Vorotyntsev" Date: Sun, 4 Feb 2024 03:49:31 +0200 Subject: [PATCH] Fix JA4SSH and JA4H (#69) * [fix] Generate a JA4SSH fingerprint every 200 _SSH_ packets * Fix calculation of JA4H_c Related issue: #58 --- pcap/badcurveball.pcap | Bin 0 -> 3348 bytes pcap/single-packets.pcap | Bin 0 -> 6472 bytes pcap/tls-alpn-h2.pcap | Bin 0 -> 3827 bytes rust/CHANGELOG.md | 10 ++- rust/Cargo.lock | 4 +- rust/Cargo.toml | 2 +- rust/ja4/config.toml | 2 +- rust/ja4/src/conf.rs | 4 +- rust/ja4/src/http.rs | 67 ++++++++--------- .../ja4__insta@badcurveball.pcap.snap | 35 +++++++++ .../ja4__insta@single-packets.pcap.snap | 69 ++++++++++++++++++ .../src/snapshots/ja4__insta@ssh-r.pcap.snap | 18 ++--- .../ja4__insta@ssh-scp-1050.pcap.snap | 10 +-- .../src/snapshots/ja4__insta@ssh2.pcapng.snap | 5 +- .../ja4__insta@tls-alpn-h2.pcap.snap | 41 +++++++++++ rust/ja4/src/ssh.rs | 34 ++++----- 16 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 pcap/badcurveball.pcap create mode 100644 pcap/single-packets.pcap create mode 100644 pcap/tls-alpn-h2.pcap create mode 100644 rust/ja4/src/snapshots/ja4__insta@badcurveball.pcap.snap create mode 100644 rust/ja4/src/snapshots/ja4__insta@single-packets.pcap.snap create mode 100644 rust/ja4/src/snapshots/ja4__insta@tls-alpn-h2.pcap.snap diff --git a/pcap/badcurveball.pcap b/pcap/badcurveball.pcap new file mode 100644 index 0000000000000000000000000000000000000000..235d1c2e19ecfd89fff5ba606db83cacfb873cdb GIT binary patch literal 3348 zcmdUyc~DeG9>;(0nBf6~9K(G%Y7%u2rr$8AfT9Q}5YQmOYfFyN5kv*Ur5S+$2Gju* zjMP%DpnxZ6VgxOsM%f^sh`0tXB&Y#n)PTp9D_S0GH-o5nRBe_2?3?L%{p;87)4$j6 z`}=jz`Pu^|G_WvmOa`E^vC?*UXjC=_({Y^?Fly^HqH;3xRGAw%@Bx@izLSNLl)V{g z0b@H3X)c>nM@o8J$B@qZ>E2$|IRKg!L{khSq$o}={7^|QxyI63`s#cR{IN}nma843 zk3NCW?+WA62$_sP9l31oe?ZfWM{wL`5_PTemp*zGOLKbB?N(|+$0O+wzq3j?n1ZRK zyydr|-|uyU{C33jvuQ@0f!BOI16qIc37HUcF>x9VePu9_cGVK?zkMgsk~5GLy-74@ zC>l>P^^9Ta_G>irtcUV{9Q{|Od+gjzfiHf^)B8#)4WC;*vM%RgnnXu4zW6~>N*UYw zyQpE0w{^UacyXYci%08B>4|T)Xtu-f@^UdHro~K+LyfFvObu$<)3m!rTw@Abpa^8( z0(y9;e19^{0M=j$V5W>%=(Hd)E=i?~Sh_Sp6`$a=Aa)s80QBOn0DAbtH8z4BZn6Y@ zpmEy>GL=Bp#BZWyx*1Ng$mNhuy@hE(YzMfRdunfb{ z3AOpHs2(ztguMe zWBKP3B7d8MZ35^kgNcHTHnRyasO5dc+SUPL9wC}D419rQAHXo@4juh+cX8xikJdxg zpB2tlRy8ec?<@{f7fdmcH%zPZh8hQePsl6LYDgbg2C&Svy3q&S0nuQn;I=*p{s2J- z(2*#EUVt*FJKGqVqG{96)5&*pN`=RZYjj4f-Q2p>OL?{x=?OV$mT&V!mZ6asua9(v zOb9C!)4nr#Lu5bIt@7yPmlBNA%ckiCTgKOQ;D z6=)pqqL9zCFpwi`I`^As=4*q51oI=%FfG$M*mHWY#4jd66&IsQkOU{h#x0Y0sff+n zBU{pm5t(T_1$#-ngQFI!mM)Zhi9-w}VaIg(9)qTY0*7=KhBHcF?aZngzdttoTMv5& zvAfxo!08jhl4Sus@#-r(T;~^EO3n>iJfrFJM~y+D?J{+j*sXrlsZ~GK+h$gDF0pue zDK_5W(G!EAKHV|W)N>o#oLZ9V{Cad}&#CWjiU=54WVC!;S^5T)p3^sFc4C%BfF#&W zJ7d-gS!BFB6CgX%V+d=`ng;ot{5ObW9Tv@8FsWs9S6zfx;8dhfE*rA=Z3wO-o*199 zQZ!1)IS46%K!>3QYV<&j8K|+ZzfL9+&_Wu~_fF324Ep~dI!B#P2`e|BYjHK{>uf_xX{!63nnv*l56<^dPQR4iVt;?7x2FX(T%5bwe_v{L zWyseyxq9A1w_di5@%MPxKI!;Bo2m~40h5R_n6I$f9`!3tshG_7R4_N6$dj)+dv57@ zs(WqI>#lt?rD19D{8(OrkPFf(P0qXYn;&P845|r-(82+EVPiHY3q{d9^nUnw zK`y^csiclpv51^Ma16fZ8H*};*{%+cKW`MS7cA*~>}X|awFoW$lr$^|On*4e@!A|~g9E<% zcd5I&tfHIhM#tkhB_aEfJR|wK3g_!vcugZ^aK5Il*vujMIz;nkzRq*(TLCPgO?KwZ zeEr^MEel3LuSK=jz2$4Fi;j1#wfe38#F9jVM1?blhLS)Y!CE5oG}Cu>FeDws6xGWL z5Q9&z2Me}pe`<|yvHEeiMTxQ#GXKTG>NN`qP(J>cB}noHMhDAgNfhl5FBCTz1Kf{wek<% zP{Mn*sP1zA1Nkj!MmkU2CJt;UcjDDQj}S%CBJjg>Qr;rE_iuVL-rS=06D>J-;Q5kR znnj3a*jbu(B49BF|F64gch^2wjX7||e;%~EX)Tx#H8HP`55#M_F_f4l8_VGO4@xyrKfRCE!cIy@Y1?qD*82|tP literal 0 HcmV?d00001 diff --git a/pcap/single-packets.pcap b/pcap/single-packets.pcap new file mode 100644 index 0000000000000000000000000000000000000000..bc0583517194912b57dbf34a78061be135dce02e GIT binary patch literal 6472 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 literal 0 HcmV?d00001 diff --git a/pcap/tls-alpn-h2.pcap b/pcap/tls-alpn-h2.pcap new file mode 100644 index 0000000000000000000000000000000000000000..74bf35acb57a00145bb2b0363acd7ad94019b202 GIT binary patch literal 3827 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 hKTUEFeP&b{J@UOfmYZU2n#7N=gL?n~ literal 0 HcmV?d00001 diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index 0933c85..a1546b6 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.18.0] - 2024-02-04 + +### Fixed + +- Generate a JA4SSH fingerprint every 200 *SSH* (layer 7) packets. +- Fix calculation of JA4H\_c (#58). + ## [0.17.0] - 2024-01-31 ### Fixed @@ -70,7 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Rust sources of `ja4` and `ja4x` CLI tools. -[unreleased]: https://github.com/FoxIO-LLC/ja4/compare/v0.17.0...HEAD +[unreleased]: https://github.com/FoxIO-LLC/ja4/compare/v0.18.0...HEAD +[0.18.0]: https://github.com/FoxIO-LLC/ja4/compare/v0.17.0...v0.18.0 [0.17.0]: https://github.com/FoxIO-LLC/ja4/compare/v0.16.2...v0.17.0 [0.16.2]: https://github.com/FoxIO-LLC/ja4/compare/v0.16.1...v0.16.2 [0.16.1]: https://github.com/FoxIO-LLC/ja4/compare/v0.16.0...v0.16.1 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7ad4dfc..444fcd9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -524,7 +524,7 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "ja4" -version = "0.17.0" +version = "0.18.0" dependencies = [ "clap", "color-eyre", @@ -552,7 +552,7 @@ dependencies = [ [[package]] name = "ja4x" -version = "0.17.0" +version = "0.18.0" dependencies = [ "clap", "color-eyre", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index de2b496..bd204f1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ members = ["ja4", "ja4x"] resolver = "2" [workspace.package] -version = "0.17.0" +version = "0.18.0" license = "LicenseRef-FoxIO-Proprietary" repository = "https://github.com/FoxIO-LLC/ja4" diff --git a/rust/ja4/config.toml b/rust/ja4/config.toml index 2f3e5e3..54757fe 100644 --- a/rust/ja4/config.toml +++ b/rust/ja4/config.toml @@ -7,7 +7,7 @@ [ssh] # enabled = true -## How many packets to collect per SSH TCP stream before generating a JA4SSH fingerprint +## New JA4SSH fingerprint is generated every `sample_size` SSH packets # sample_size = 200 diff --git a/rust/ja4/src/conf.rs b/rust/ja4/src/conf.rs index 417bbe0..ce0f14e 100644 --- a/rust/ja4/src/conf.rs +++ b/rust/ja4/src/conf.rs @@ -28,11 +28,11 @@ pub(crate) struct ConfSsh { pub(crate) enabled: bool, /// JA4SSH (SSH traffic fingerprinting) runs every `sample_size` packets /// per SSH TCP stream. - pub(crate) sample_size: u32, + pub(crate) sample_size: usize, } impl ConfSsh { - const DEFAULT_SAMPLE_SIZE: u32 = 200; + const DEFAULT_SAMPLE_SIZE: usize = 200; fn prepare(mut self) -> Self { if self.enabled && self.sample_size == 0 { diff --git a/rust/ja4/src/http.rs b/rust/ja4/src/http.rs index ec00d37..30955f9 100644 --- a/rust/ja4/src/http.rs +++ b/rust/ja4/src/http.rs @@ -187,15 +187,19 @@ impl HttpStats { let first_chunk = format!("{req_method}{version}{cookie_marker}{referer_marker}{nr_headers:02}{lang}"); + let mut cookie_names = cookie_names(&cookies).collect_vec(); + if !original_order { + cookie_names.sort_unstable(); cookies.sort_unstable(); } let headers = headers.into_iter().join(","); - let (cookie_keys, cookie_items) = cookie_keys_and_items(&cookies); + let cookie_names = cookie_names.into_iter().join(","); + let cookies = cookies.into_iter().join(","); let ja4h_r = with_raw.then(|| { - let s = format!("{first_chunk}_{headers}_{cookie_keys}_{cookie_items}"); + let s = format!("{first_chunk}_{headers}_{cookie_names}_{cookies}"); if original_order { Ja4hRawFingerprint::Unsorted(s) } else { @@ -204,10 +208,10 @@ impl HttpStats { }); let headers = crate::hash12(headers); - let cookie_keys = crate::hash12(cookie_keys); - let cookie_items = crate::hash12(cookie_items); + let cookie_names = crate::hash12(cookie_names); + let cookies = crate::hash12(cookies); let ja4h = { - let s = format!("{first_chunk}_{headers}_{cookie_keys}_{cookie_items}"); + let s = format!("{first_chunk}_{headers}_{cookie_names}_{cookies}"); if original_order { Ja4hFingerprint::Unsorted(s) } else { @@ -223,47 +227,36 @@ impl HttpStats { } } -/// Returns a comma-separated list of cookie keys ("key1,key2,key3") and a comma-separated -/// list of cookies ("key1=value1,key2=value2,key3=value3") . -fn cookie_keys_and_items>(cookies: &[S]) -> (String, String) { - cookies - .iter() - .fold((String::new(), String::new()), |(keys, items), cookie| { - let cookie = cookie.as_ref(); - // SAFETY: `split` never returns an empty iterator, so it's safe to unwrap. - let key = cookie.split('=').next().unwrap(); - - let keys = if keys.is_empty() { - key.to_owned() - } else { - format!("{keys},{key}") - }; - - let items = if items.is_empty() { - cookie.to_owned() - } else { - format!("{items},{cookie}") - }; - - (keys, items) - }) +/// Returns an iterator of owned cookie names. +fn cookie_names>(cookies: &[S]) -> impl Iterator + '_ { + cookies.iter().map(|cookie| { + // SAFETY: `split` never returns an empty iterator, so it's safe to unwrap. + cookie.as_ref().split('=').next().unwrap().to_owned() + }) } #[test] -fn test_cookie_keys_and_items() { +fn test_cookie_names() { + let no_cookies: [&str; 0] = []; + assert!(cookie_names(&no_cookies).next().is_none()); + assert_eq!( - cookie_keys_and_items(&["foo=bar", "baz=qux"]), - ("foo,baz".to_owned(), "foo=bar,baz=qux".to_owned()) + cookie_names(&["foo=bar", "baz=qux"]).collect::>(), + ["foo", "baz"] ); + assert_eq!( - cookie_keys_and_items(&["a=5", "c=3", "b=2", "a=1", "a=4"]), - ("a,c,b,a,a".to_owned(), "a=5,c=3,b=2,a=1,a=4".to_owned()) + cookie_names(&["a=5", "c=3=4=5", "b=2", "a=1", "a=4"]).collect::>(), + ["a", "c", "b", "a", "a"] ); - let no_cookies: [&str; 0] = []; assert_eq!( - cookie_keys_and_items(&no_cookies), - (String::new(), String::new()) + cookie_names(&[ + "pardot=tee2foreb3fefpgvk8u1056vt3", + "visitor_id413862-hash=1f00bdb076b5fb707c70254849819ec1797d3e27cef91a61a9488cb7ca0ebf77f226caa4075591b2591bf9a1ccdf29432c67379b", + "visitor_id413862=286585660", + ]).collect_vec(), + ["pardot", "visitor_id413862-hash", "visitor_id413862"] ); } diff --git a/rust/ja4/src/snapshots/ja4__insta@badcurveball.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@badcurveball.pcap.snap new file mode 100644 index 0000000..2c246fb --- /dev/null +++ b/rust/ja4/src/snapshots/ja4__insta@badcurveball.pcap.snap @@ -0,0 +1,35 @@ +--- +source: ja4/src/lib.rs +expression: output +--- +- stream: 0 + transport: tcp + src: 172.130.128.76 + dst: 54.226.182.138 + src_port: 55318 + dst_port: 443 + tls_server_name: bad.curveballtest.com + ja4: t13d1615h2_46e7e9700bed_45f260be83e2 + ja4s: t1205h1_c02b_845f7282a956 + tls_certs: + - x509: + - ja4x: 2e9214a636bc_a373a9f83c6b_0e17604154c5 + issuerCountryName: HR + issuerStateOrProvinceName: Zagreb + issuerOrganizationName: INFIGO IS + issuerCommonName: INFIGO + subjectCountryName: US + subjectOrganizationName: SANS Internet Storm Center + subjectCommonName: SANS ISC DShield Test + - ja4x: 2e9214a636bc_2e9214a636bc_795797892f9c + issuerCountryName: HR + issuerStateOrProvinceName: Zagreb + issuerOrganizationName: INFIGO IS + issuerCommonName: INFIGO + subjectCountryName: HR + subjectStateOrProvinceName: Zagreb + subjectOrganizationName: INFIGO IS + subjectCommonName: INFIGO + ja4l_c: 2177_64 + ja4l_s: 781_238 + diff --git a/rust/ja4/src/snapshots/ja4__insta@single-packets.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@single-packets.pcap.snap new file mode 100644 index 0000000..93721d3 --- /dev/null +++ b/rust/ja4/src/snapshots/ja4__insta@single-packets.pcap.snap @@ -0,0 +1,69 @@ +--- +source: ja4/src/lib.rs +expression: output +--- +- stream: 0 + transport: tcp + src: 192.168.25.150 + dst: 74.125.24.149 + src_port: 49677 + dst_port: 80 + http: + - ja4h: ge11cr06enus_8c2f9ef95269_2a79f5d9f8b3_7b4d78c057bc +- stream: 1 + transport: tcp + src: 192.168.25.150 + dst: 118.215.80.242 + src_port: 49654 + dst_port: 80 + http: + - ja4h: ge11cr07enus_45c71a3fb6ea_a25bf252eb59_43a9e3e95c85 +- stream: 2 + transport: tcp + src: 192.168.25.150 + dst: 13.115.50.210 + src_port: 49683 + dst_port: 80 + http: + - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000 +- stream: 3 + transport: tcp + src: 192.168.25.150 + dst: 104.89.119.175 + src_port: 49708 + dst_port: 80 + http: + - ja4h: po11cr09enus_130d8cd1913c_f81c0e5c6793_90689f748de6 +- stream: 4 + transport: tcp + src: 192.168.25.150 + dst: 193.242.192.43 + src_port: 49735 + dst_port: 80 + http: + - ja4h: ge11cr07enus_45c71a3fb6ea_9ee64e91aa30_109254663367 +- stream: 5 + transport: tcp + src: 192.168.25.150 + dst: 74.125.24.100 + src_port: 49733 + dst_port: 80 + http: + - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000 +- stream: 6 + transport: tcp + src: 192.168.25.150 + dst: 74.125.24.95 + src_port: 49743 + dst_port: 80 + http: + - ja4h: ge11nr06enus_8c2f9ef95269_000000000000_000000000000 +- stream: 7 + transport: tcp + src: 192.168.25.150 + dst: 35.174.150.168 + src_port: 49738 + dst_port: 80 + http: + - ja4h: ge11cr06enus_8c2f9ef95269_d23bf79698dc_c1eaa758c543 + diff --git a/rust/ja4/src/snapshots/ja4__insta@ssh-r.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@ssh-r.pcap.snap index 4a86440..3888fb3 100644 --- a/rust/ja4/src/snapshots/ja4__insta@ssh-r.pcap.snap +++ b/rust/ja4/src/snapshots/ja4__insta@ssh-r.pcap.snap @@ -11,9 +11,8 @@ expression: output ja4l_c: 94_128 ja4l_s: 32_64 ja4ssh: - - c64s64_c78s65_c45s10 - - c64s64_c59s72_c67s2 - - c64s64_c3s4_c4s0 + - c64s64_c107s93_c74s10 + - c64s64_c33s48_c42s2 ssh_extras: hassh: e77c2db7432e8cfbc42a96909a84fc8e hassh_server: 6832f1ce43d4397c2c0a3e2f8c94334e @@ -45,14 +44,11 @@ expression: output ja4l_c: 12_64 ja4l_s: 3169_116 ja4ssh: - - c76s76_c63s69_c19s47 - - c76s76_c74s57_c0s69 - - c76s76_c71s60_c0s69 - - c76s76_c69s62_c0s69 - - c76s76_c71s59_c0s70 - - c76s76_c75s57_c0s68 - - c76s76_c70s69_c6s55 - - c36s60_c3s3_c4s1 + - c76s76_c104s96_c19s82 + - c76s76_c108s92_c0s105 + - c76s76_c106s94_c0s107 + - c76s76_c111s89_c0s102 + - c76s76_c67s65_c10s52 ssh_extras: hassh: ec9ea89c70f5fc71cf61061bff5e4740 hassh_server: 2307c390c7c9aba5b4c9519e72347f34 diff --git a/rust/ja4/src/snapshots/ja4__insta@ssh-scp-1050.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@ssh-scp-1050.pcap.snap index 2f66914..d36ff5b 100644 --- a/rust/ja4/src/snapshots/ja4__insta@ssh-scp-1050.pcap.snap +++ b/rust/ja4/src/snapshots/ja4__insta@ssh-scp-1050.pcap.snap @@ -11,11 +11,11 @@ expression: output ja4l_c: 179_128 ja4l_s: 38_64 ja4ssh: - - c112s80_c52s107_c35s4 - - c0s1460_c0s174_c26s0 - - c112s1460_c13s150_c37s0 - - c0s1460_c0s178_c22s0 - - c0s1460_c0s179_c21s0 + - c112s1460_c52s148_c41s4 + - c112s1460_c13s187_c35s0 + - c0s1460_c0s200_c36s0 + - c0s1460_c0s200_c23s0 + - c0s1460_c0s53_c6s0 ssh_extras: hassh: eb6d4c713c7dcaba7cfd070b095213a9 hassh_server: 6832f1ce43d4397c2c0a3e2f8c94334e diff --git a/rust/ja4/src/snapshots/ja4__insta@ssh2.pcapng.snap b/rust/ja4/src/snapshots/ja4__insta@ssh2.pcapng.snap index d469afe..52cc3c8 100644 --- a/rust/ja4/src/snapshots/ja4__insta@ssh2.pcapng.snap +++ b/rust/ja4/src/snapshots/ja4__insta@ssh2.pcapng.snap @@ -172,9 +172,8 @@ expression: output ja4l_c: 77_128 ja4l_s: 12897_50 ja4ssh: - - c36s36_c55s87_c51s5 - - c36s36_c49s90_c59s2 - - c36s36_c14s23_c15s0 + - c36s36_c76s124_c74s5 + - c36s52_c42s76_c51s2 ssh_extras: hassh: 06046964c022c6407d15a27b12a6a4fb hassh_server: 699519fdcc30cbcd093d5cd01e4b1d56 diff --git a/rust/ja4/src/snapshots/ja4__insta@tls-alpn-h2.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@tls-alpn-h2.pcap.snap new file mode 100644 index 0000000..a209889 --- /dev/null +++ b/rust/ja4/src/snapshots/ja4__insta@tls-alpn-h2.pcap.snap @@ -0,0 +1,41 @@ +--- +source: ja4/src/lib.rs +expression: output +--- +- stream: 0 + transport: tcp + src: 2001:4998:ef83:14:8000::100d + dst: 2606:4700::6811:d209 + src_port: 64034 + dst_port: 443 + tls_server_name: www.cloudflare.com + ja4: t12d4605h2_85626a9a5f7f_aaf95bb78ec9 + ja4s: t1204h2_cca9_1428ce7b4018 + tls_certs: + - x509: + - ja4x: 7d5dbb3783b4_ba7ce0880c07_7bf9a7bf7029 + issuerCountryName: US + issuerOrganizationName: DigiCert Inc + issuerOrganizationalUnit: www.digicert.com + issuerCommonName: DigiCert ECC Extended Validation Server CA + subjectBusinessCategory: Private Organization + subjectMsJurisdictionCountry: US + subjectMsJurisdictionStateOrProvince: Delaware + subjectSerialNumber: '4710875' + subjectCountryName: US + subjectStateOrProvinceName: California + subjectLocalityName: San Francisco + subjectOrganizationName: Cloudflare, Inc. + subjectCommonName: cloudflare.com + - ja4x: 7d5dbb3783b4_7d5dbb3783b4_41a019652939 + issuerCountryName: US + issuerOrganizationName: DigiCert Inc + issuerOrganizationalUnit: www.digicert.com + issuerCommonName: DigiCert High Assurance EV Root CA + subjectCountryName: US + subjectOrganizationName: DigiCert Inc + subjectOrganizationalUnit: www.digicert.com + subjectCommonName: DigiCert ECC Extended Validation Server CA + ja4l_c: 35_64 + ja4l_s: 18861_59 + diff --git a/rust/ja4/src/ssh.rs b/rust/ja4/src/ssh.rs index 5e2dd84..8ab24be 100644 --- a/rust/ja4/src/ssh.rs +++ b/rust/ja4/src/ssh.rs @@ -16,7 +16,7 @@ use crate::{Packet, Result, Sender}; #[derive(Debug, Default)] pub(crate) struct Stream { /// Statistics obtained from up to [`crate::conf::ConfSsh::sample_size`] packets. - counts: PacketCounts, + stats: Stats, /// SSH fingerprints. /// /// New entry is added every [`crate::conf::ConfSsh::sample_size`] packets. @@ -25,11 +25,16 @@ pub(crate) struct Stream { } impl Stream { - pub(crate) fn update(&mut self, pkt: &Packet, sender: Sender, sample_size: u32) -> Result<()> { - self.counts.update(pkt, sender)?; - if self.counts.nr_packets == sample_size { - let counts = std::mem::take(&mut self.counts); - if let Some(fp) = counts.into() { + pub(crate) fn update( + &mut self, + pkt: &Packet, + sender: Sender, + sample_size: usize, + ) -> Result<()> { + self.stats.update(pkt, sender)?; + if self.stats.nr_ssh_client_packets + self.stats.nr_ssh_server_packets == sample_size { + let stats = std::mem::take(&mut self.stats); + if let Some(fp) = stats.into() { self.ja4ssh.push(fp); } } @@ -39,7 +44,7 @@ impl Stream { pub(crate) fn finish(self) -> (Vec, Option) { let Stream { - counts, + stats: counts, mut ja4ssh, extras, } = self; @@ -190,9 +195,7 @@ impl StreamExtras { } #[derive(Debug, Default)] -struct PacketCounts { - /// How many packets contributed to this `Stats` instance? - nr_packets: u32, +struct Stats { /// Key -- client TCP payload length, bytes; value -- number of packets with this length. /// Notes: /// @@ -213,7 +216,7 @@ struct PacketCounts { nr_tcp_server_acks: usize, } -impl PacketCounts { +impl Stats { fn update(&mut self, pkt: &Packet, sender: Sender) -> Result<()> { const BARE_ACK_FLAG: &str = "0x0010"; @@ -239,8 +242,6 @@ impl PacketCounts { Sender::Server => self.nr_tcp_server_acks += 1, } } - - self.nr_packets += 1; Ok(()) } } @@ -249,10 +250,9 @@ impl PacketCounts { #[derive(Debug, Serialize)] pub(crate) struct Fingerprint(String); -impl From for Option { - fn from(counters: PacketCounts) -> Self { - let PacketCounts { - nr_packets: _, +impl From for Option { + fn from(counters: Stats) -> Self { + let Stats { client_tcp_len_counts, server_tcp_len_counts, nr_ssh_client_packets,