diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e4beaf3..4306406 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -59,7 +59,7 @@ jobs: # Specify `--manifest-path` as a workaround. # # See https://github.com/actions-rs/clippy-check/issues/28 - args: --all-features --workspace --manifest-path rust/Cargo.toml + args: --all-features --workspace --manifest-path rust/Cargo.toml -- -D warnings token: ${{ secrets.GITHUB_TOKEN }} doc: diff --git a/pcap/tls12.pcap b/pcap/tls12.pcap new file mode 100644 index 0000000..c22ee8a Binary files /dev/null and b/pcap/tls12.pcap differ diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index be5e9e0..89c5b18 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -7,10 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.16.2] - 2024-01-04 + ### Fixed - JA4: Include SNI (0000) and ALPN (0010) in the "original" outputs (#40). - JA4H: Search for "Cookie" and "Referer" fields in a case-insensitive fashion. +- JA4: Take signature algorithm hex values from `signature_algorithms` extension only (#41). ## [0.16.1] - 2023-12-22 @@ -53,7 +56,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.16.1...HEAD +[unreleased]: https://github.com/FoxIO-LLC/ja4/compare/v0.16.2...HEAD +[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 [0.16.0]: https://github.com/FoxIO-LLC/ja4/compare/v0.15.2...v0.16.0 [0.15.2]: https://github.com/FoxIO-LLC/ja4/compare/v0.15.1...v0.15.2 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index aeaf7cf..e858274 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -531,7 +531,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "ja4" -version = "0.16.1" +version = "0.16.2" dependencies = [ "clap", "color-eyre", @@ -559,7 +559,7 @@ dependencies = [ [[package]] name = "ja4x" -version = "0.16.1" +version = "0.16.2" dependencies = [ "clap", "color-eyre", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 05b819b..975286b 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,7 +3,7 @@ members = ["ja4", "ja4x"] resolver = "2" [workspace.package] -version = "0.16.1" +version = "0.16.2" license = "LicenseRef-FoxIO-Proprietary" repository = "https://github.com/FoxIO-LLC/ja4" diff --git a/rust/ja4/src/pcap.rs b/rust/ja4/src/pcap.rs index 56955e6..1467a43 100644 --- a/rust/ja4/src/pcap.rs +++ b/rust/ja4/src/pcap.rs @@ -74,6 +74,11 @@ impl Proto<'_> { self.inner.name() } + /// Returns an iterator over all [`rtshark::Metadata`] for this protocol. + pub(crate) fn iter(&self) -> impl Iterator { + self.inner.iter() + } + /// Returns an iterator over the sequence of [`rtshark::Metadata`] with the given [name]. /// /// [name]: rtshark::Metadata::name diff --git a/rust/ja4/src/snapshots/ja4__insta@tls12.pcap.snap b/rust/ja4/src/snapshots/ja4__insta@tls12.pcap.snap new file mode 100644 index 0000000..8e67212 --- /dev/null +++ b/rust/ja4/src/snapshots/ja4__insta@tls12.pcap.snap @@ -0,0 +1,13 @@ +--- +source: ja4/src/lib.rs +expression: output +--- +- stream: 0 + transport: tcp + src: 192.168.133.129 + dst: 34.117.237.239 + src_port: 36372 + dst_port: 443 + tls_server_name: contile.services.mozilla.com + ja4: t13d1715h2_5b57614c22b0_3d5424432f57 + diff --git a/rust/ja4/src/tls.rs b/rust/ja4/src/tls.rs index cb29b18..137a471 100644 --- a/rust/ja4/src/tls.rs +++ b/rust/ja4/src/tls.rs @@ -12,6 +12,7 @@ use std::fmt; use itertools::Itertools as _; use ja4x::x509_parser::{certificate::X509Certificate, prelude::FromDer as _}; use serde::Serialize; +use tracing::{debug, warn}; use crate::{Error, FormatFlags, Packet, PacketNum, Proto, Result}; @@ -186,23 +187,13 @@ impl ClientStats { let alpn = tls .first("tls.handshake.extensions_alpn_str") .map_or((None, None), first_last); - let sig_hash_algs = tls - .values("tls.handshake.sig_hash_alg") - .filter_map(|v| { - let s = v.strip_prefix("0x"); - if s.is_none() { - tracing::debug!(sig_hash_alg = v, %pkt.num, "Invalid signature algorithm"); - } - s.map(str::to_owned) - }) - .collect(); let ciphers = tls .values("tls.handshake.ciphersuite") .filter(|v| !TLS_GREASE_VALUES_STR.contains(v)) .filter_map(|v| { let s = v.strip_prefix("0x"); if s.is_none() { - tracing::debug!(cipher = v, %pkt.num, "Invalid cipher suite"); + debug!(cipher = v, %pkt.num, "Invalid cipher suite"); } s.map(str::to_owned) }) @@ -215,7 +206,7 @@ impl ClientStats { exts, sni, alpn, - sig_hash_algs, + sig_hash_algs: sig_hash_algs(pkt, tls), }) } @@ -254,6 +245,46 @@ impl ClientStats { } } +/// Returns hex values of the signature algorithms. +fn sig_hash_algs(pkt: &Packet, tls: &Proto) -> Vec { + assert_eq!(tls.name(), "tls"); + + // `signature_algorithms` is not the only TLS extension that contains + // `tls.handshake.sig_hash_alg` fields. For example, `delegated_credentials` + // contains them too; see https://github.com/FoxIO-LLC/ja4/issues/41 + // + // We are only interested in `signature_algorithms` extension, so we skip forward + // to it. + let mut iter = tls + .iter() + .skip_while(|&md| md.name() != "tls.handshake.extension.type" || md.value() != "13"); + match iter.next() { + Some(md) => debug_assert_eq!(md.display(), "Type: signature_algorithms (13)"), + None => { + debug!(%pkt.num, "signature_algorithms TLS extension not found"); + return Vec::new(); + } + } + match iter.next() { + Some(md) => debug_assert_eq!(md.name(), "tls.handshake.extension.len"), + None => { + warn!(%pkt.num, "Unexpected end of TLS dissection"); + return Vec::new(); + } + } + + iter.take_while(|&md| md.name().starts_with("tls.handshake.sig_hash_")) + .filter(|&md| md.name() == "tls.handshake.sig_hash_alg") + .filter_map(|md| { + let s = md.value().strip_prefix("0x"); + if s.is_none() { + warn!(%pkt.num, ?md, "signature algorithm value doesn't start with \"0x\""); + } + s.map(str::to_owned) + }) + .collect() +} + /// Pieces of data that is used to construct [`Ja4Fingerprint`] and [`Ja4RawFingerprint`]. #[derive(Debug)] struct PartsOfClientFingerprint { @@ -394,7 +425,7 @@ impl ServerStats { let v = tls.first("tls.handshake.ciphersuite")?; let Some(cipher) = v.strip_prefix("0x") else { - tracing::debug!(cipher = v, %pkt.num, "Invalid cipher suite"); + debug!(cipher = v, %pkt.num, "Invalid cipher suite"); return Ok(None); }; @@ -563,7 +594,7 @@ fn tls_extensions_client(tls: &Proto) -> Vec { Ok(n) if TLS_GREASE_VALUES_INT.contains(&n) => None, Ok(n) => Some(n), Err(error) => { - tracing::debug!(packet = %tls.packet_num, value = md.value(), showname = md.display(), %error, "Invalid TLS extension"); + debug!(packet = %tls.packet_num, value = md.value(), showname = md.display(), %error, "Invalid TLS extension"); None } } @@ -579,7 +610,7 @@ fn tls_extensions_server(tls: &Proto) -> Vec { tls.fields("tls.handshake.extension.type").filter_map(|md| { md.value().parse::().map_err(|e| { - tracing::debug!(packet = %tls.packet_num, value = md.value(), showname = md.display(), error = %e, "Invalid TLS extension"); + debug!(packet = %tls.packet_num, value = md.value(), showname = md.display(), error = %e, "Invalid TLS extension"); }).ok() }) .collect()