use md5::{Digest, Md5};
pub fn compute_ja3(
tls_version: u16,
ciphers: &[u16],
extensions: &[u16],
curves: &[u16],
ec_point_formats: &[u8],
) -> (String, String) {
fn join_u16(items: &[u16]) -> String {
items
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join("-")
}
fn join_u8(items: &[u8]) -> String {
items
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join("-")
}
let s = format!(
"{tls_version},{ciphers},{exts},{curves},{ec_fmts}",
ciphers = join_u16(ciphers),
exts = join_u16(extensions),
curves = join_u16(curves),
ec_fmts = join_u8(ec_point_formats),
);
let mut hasher = Md5::new();
hasher.update(s.as_bytes());
let digest = hasher.finalize();
let hash = digest
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
(s, hash)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ja3_canonical_chrome_example() {
let (s, h) = compute_ja3(
771,
&[4865, 4866, 4867],
&[0, 23, 65281],
&[29, 23, 24],
&[0],
);
assert_eq!(s, "771,4865-4866-4867,0-23-65281,29-23-24,0");
assert_eq!(h.len(), 32);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn ja3_empty_fields() {
let (s, h) = compute_ja3(771, &[], &[], &[], &[]);
assert_eq!(s, "771,,,,");
assert_eq!(h.len(), 32);
}
#[test]
fn ja3_hash_deterministic() {
let (_, h1) = compute_ja3(771, &[4865], &[0], &[29], &[0]);
let (_, h2) = compute_ja3(771, &[4865], &[0], &[29], &[0]);
assert_eq!(h1, h2);
}
}